refactor: test and clean

This commit is contained in:
cha0s 2022-09-15 09:00:49 -05:00
parent a70b8c2d76
commit da6d15e910
8 changed files with 431 additions and 373 deletions

View File

@ -1,375 +1,17 @@
/* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */ /* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */
import ArbitraryComponent from './component/arbitrary';
import FlatComponent from './component/flat';
import Schema from './schema'; import Schema from './schema';
import Serializer from './serializer';
class BaseComponent {
$$dirty = true;
map = [];
pool = [];
schema;
serializer;
constructor(schema) {
this.schema = schema;
this.serializer = new Serializer(schema);
}
allocate() {
const [index] = this.allocateMany(1);
return index;
}
allocateMany(count) {
const results = [];
// eslint-disable-next-line no-param-reassign
while (count-- > 0 && this.pool.length > 0) {
results.push(this.pool.pop());
}
return results;
}
create(entity, values) {
const [result] = this.createMany([entity, values]);
return result;
}
destroy(entity) {
this.destroyMany([entity]);
}
destroyMany(entities) {
this.freeMany(entities.map((entity) => this.map[entity]).filter((index) => !!index));
for (let i = 0; i < entities.length; i++) {
this.map[entities[i]] = undefined;
}
}
get dirty() {
return this.$$dirty;
}
setDirty(dirty) {
this.$$dirty = dirty;
}
free(index) {
this.freeMany([index]);
}
freeMany(indices) {
for (let i = 0; i < indices.length; ++i) {
this.pool.push(indices[i]);
}
}
set(entity, values) {
const instance = this.getUnsafe(entity);
for (const i in values) {
instance[i] = values[i];
}
}
}
class FlatComponent extends BaseComponent {
chunkSize = 64;
caret = 0;
data = new ArrayBuffer(0);
Window;
window;
allocateMany(count) {
const results = super.allocateMany(count);
// eslint-disable-next-line no-param-reassign
count -= results.length;
if (count > 0) {
const required = (this.caret + count) * this.constructor.width;
if (required > this.data.byteLength) {
const chunkWidth = this.chunkSize * this.constructor.width;
const remainder = required % chunkWidth;
const extra = 0 === remainder ? 0 : chunkWidth - remainder;
const size = required + extra;
const data = new ArrayBuffer(size);
(new Uint8Array(data)).set(this.data);
this.data = data;
}
for (let i = 0; i < count; ++i) {
results.push(this.caret++);
}
}
return results;
}
clean() {
if (!this.dirty) {
return;
}
if (!this.Window) {
this.Window = this.makeWindowClass();
}
const window = new this.Window(this.data, this);
for (let i = 0; i < this.caret; ++i) {
window.dirty = false;
window.cursor += this.constructor.width;
}
this.setDirty(0, 0);
}
createMany(entries) {
if (entries.length > 0) {
const allocated = this.allocateMany(entries.length);
if (!this.Window) {
this.Window = this.makeWindowClass();
}
const window = new this.Window(this.data, this);
const {defaultValues} = this.schema;
for (let i = 0; i < entries.length; ++i) {
let entity;
let values = {};
if (Array.isArray(entries[i])) {
[entity, values] = entries[i];
}
else {
entity = entries[i];
}
this.map[entity] = allocated[i];
window.cursor = allocated[i] * this.constructor.width;
window.entity = entity;
for (const [i] of this.schema) {
if (i in values) {
window[i] = values[i];
}
else if (i in defaultValues) {
window[i] = defaultValues[i];
}
}
}
}
}
get(entity) {
if ('undefined' === typeof this.map[entity]) {
return undefined;
}
if (!this.Window) {
this.Window = this.makeWindowClass();
}
const window = new this.Window(this.data, this);
window.cursor = this.map[entity] * this.constructor.width;
return window;
}
getUnsafe(entity) {
if ('undefined' === typeof this.map[entity]) {
return undefined;
}
if (!this.Window) {
this.Window = this.makeWindowClass();
}
if (!this.window) {
this.window = new this.Window(this.data, this);
}
this.window.cursor = this.map[entity] * this.constructor.width;
return this.window;
}
makeWindowClass() {
const Component = this;
class Window {
cursor = 0;
parent;
view;
constructor(data, parent) {
if (data) {
this.view = new DataView(data);
}
if (parent) {
this.parent = parent;
}
}
toJSON() {
const json = {};
for (const [i] of Component.schema) {
json[i] = this[i];
}
return json;
}
}
let offset = 0;
const properties = {};
const {width} = this.constructor;
const get = (type) => (
`return this.view.get${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, true);`
);
const set = (type) => [
`this.parent.setDirty(1, Number(this.view.getBigUint64(this.cursor + ${width - 9}, true)));`,
`this.view.set${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, v, true);`,
`this.view.setUint8(this.cursor + ${width - 1}, 1, true);`,
].join('');
/* eslint-disable no-new-func */
properties.dirty = {
get: new Function('', `return !!this.view.getUint8(this.cursor + ${width - 1}, true);`),
set: new Function('v', `this.view.setUint8(this.cursor + ${width - 1}, v ? 1 : 0, true);`),
};
properties.entity = {
get: new Function('', `return Number(this.view.getBigUint64(this.cursor + ${width - 9}, true));`),
set: new Function('v', `this.view.setBigUint64(this.cursor + ${width - 9}, BigInt(v), true);`),
};
for (const [i, spec] of this.schema) {
const {type} = spec;
properties[i] = {};
properties[i].get = new Function('', get(type));
properties[i].set = new Function('v', set(type));
offset += Schema.sizeOfType(type);
}
/* eslint-enable no-new-func */
Object.defineProperties(Window.prototype, properties);
return Window;
}
}
class ArbitraryComponent extends BaseComponent {
data = [];
Instance;
allocateMany(count) {
if (!this.Instance) {
this.Instance = this.instanceFromSchema();
}
const results = super.allocateMany(count);
// eslint-disable-next-line no-param-reassign
count -= results.length;
// eslint-disable-next-line no-param-reassign
while (count--) {
results.push(this.data.push(new this.Instance()) - 1);
}
return results;
}
clean() {
if (!this.dirty) {
return;
}
for (const i in this.map) {
this.data[this.map[i]].dirty = false;
}
this.setDirty(false, 0);
}
createMany(entries) {
if (entries.length > 0) {
const allocated = this.allocateMany(entries.length);
for (let i = 0; i < entries.length; ++i) {
let entity;
let values = {};
if (Array.isArray(entries[i])) {
[entity, values] = entries[i];
}
else {
entity = entries[i];
}
this.map[entity] = allocated[i];
this.data[allocated[i]].entity = entity;
for (const j in values) {
if (this.schema.has(j)) {
this.data[allocated[i]][j] = values[j];
}
}
}
}
}
get(entity) {
return this.data[this.map[entity]];
}
getUnsafe(entity) {
return this.get(entity);
}
instanceFromSchema() {
const Component = this;
const Instance = class {
$$dirty = 1;
$$entity = 0;
constructor() {
for (const [i, {defaultValue}] of Component.schema) {
this[i] = defaultValue;
}
}
toJSON() {
const json = {};
for (const [i] of Component.schema) {
json[i] = this[i];
}
return json;
}
};
const properties = {};
properties.dirty = {
get: function get() {
return !!this.$$dirty;
},
set: function set(v) {
this.$$dirty = v;
},
};
properties.entity = {
get: function get() {
return this.$$entity;
},
set: function set(v) {
this.$$entity = v;
},
};
for (const [i] of Component.schema) {
properties[i] = {
get: function get() {
return this[`$$${i}`];
},
set: function set(v) {
this[`$$${i}`] = v;
this.$$dirty = 1;
Component.setDirty(1, this.entity);
},
};
}
Object.defineProperties(Instance.prototype, properties);
return Instance;
}
}
export default function ComponentRouter() { export default function ComponentRouter() {
const schema = new Schema(this.constructor.schema); const schema = new Schema(this.constructor.schema);
let RealClass; let RealClass;
if (schema.width > 0) { if (schema.width > 0) {
RealClass = class extends FlatComponent {}; RealClass = class extends FlatComponent {
RealClass.width = schema.width + 9; // 1 for dirty, 8 for entity
static width = schema.width + 9; // 1 for dirty, 8 for entity
};
} }
else { else {
RealClass = class extends ArbitraryComponent {}; RealClass = class extends ArbitraryComponent {};

View File

@ -0,0 +1,121 @@
/* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */
import BaseComponent from './base';
export default class ArbitraryComponent extends BaseComponent {
data = [];
Instance;
allocateMany(count) {
if (!this.Instance) {
this.Instance = this.instanceFromSchema();
}
const results = super.allocateMany(count);
// eslint-disable-next-line no-param-reassign
count -= results.length;
// eslint-disable-next-line no-param-reassign
while (count--) {
results.push(this.data.push(new this.Instance()) - 1);
}
return results;
}
createMany(entries) {
if (entries.length > 0) {
const allocated = this.allocateMany(entries.length);
for (let i = 0; i < entries.length; ++i) {
let entity;
let values = {};
if (Array.isArray(entries[i])) {
[entity, values] = entries[i];
}
else {
entity = entries[i];
}
this.map[entity] = allocated[i];
this.data[allocated[i]].entity = entity;
for (const j in values) {
if (this.schema.has(j)) {
this.data[allocated[i]][j] = values[j];
}
}
}
}
}
get(entity) {
return this.data[this.map[entity]];
}
getUnsafe(entity) {
return this.get(entity);
}
instanceFromSchema() {
const Component = this;
const Instance = class {
$$dirty = 1;
$$entity = 0;
constructor() {
for (const [i, {defaultValue}] of Component.schema) {
this[i] = defaultValue;
}
}
toJSON() {
const json = {};
for (const [i] of Component.schema) {
json[i] = this[i];
}
return json;
}
};
const properties = {};
properties.dirty = {
get: function get() {
return !!this.$$dirty;
},
set: function set(v) {
this.$$dirty = v;
},
};
properties.entity = {
get: function get() {
return this.$$entity;
},
set: function set(v) {
this.$$entity = v;
},
};
for (const [i] of Component.schema) {
properties[i] = {
get: function get() {
return this[`$$${i}`];
},
set: function set(v) {
this[`$$${i}`] = v;
this.$$dirty = 1;
Component.setDirty(this.entity);
},
};
}
Object.defineProperties(Instance.prototype, properties);
return Instance;
}
setClean() {
super.setClean();
if (!this.dirty) {
return;
}
for (let i = 0; i < this.data.length; i++) {
this.data[i].dirty = false;
}
}
}

View File

@ -0,0 +1,79 @@
/* eslint-disable no-restricted-syntax, guard-for-in */
import Serializer from '../serializer';
export default class BaseComponent {
$$dirty = true;
map = [];
pool = [];
schema;
serializer;
constructor(schema) {
this.schema = schema;
this.serializer = new Serializer(schema);
}
allocate() {
const [index] = this.allocateMany(1);
return index;
}
allocateMany(count) {
const results = [];
// eslint-disable-next-line no-param-reassign
while (count-- > 0 && this.pool.length > 0) {
results.push(this.pool.pop());
}
return results;
}
create(entity, values) {
this.createMany([entity, values]);
}
destroy(entity) {
this.destroyMany([entity]);
}
destroyMany(entities) {
this.freeMany(entities.map((entity) => this.map[entity]).filter((index) => !!index));
for (let i = 0; i < entities.length; i++) {
this.map[entities[i]] = undefined;
}
}
get dirty() {
return this.$$dirty;
}
free(index) {
this.freeMany([index]);
}
freeMany(indices) {
for (let i = 0; i < indices.length; ++i) {
this.pool.push(indices[i]);
}
}
set(entity, values) {
const instance = this.getUnsafe(entity);
for (const i in values) {
instance[i] = values[i];
}
}
setClean() {
this.$$dirty = false;
}
setDirty() {
this.$$dirty = true;
}
}

View File

@ -0,0 +1,172 @@
/* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */
import BaseComponent from './base';
import Schema from '../schema';
export default class FlatComponent extends BaseComponent {
chunkSize = 64;
caret = 0;
data = new ArrayBuffer(0);
Window;
window;
allocateMany(count) {
const results = super.allocateMany(count);
// eslint-disable-next-line no-param-reassign
count -= results.length;
if (count > 0) {
const required = (this.caret + count) * this.constructor.width;
if (required > this.data.byteLength) {
const chunkWidth = this.chunkSize * this.constructor.width;
const remainder = required % chunkWidth;
const extra = 0 === remainder ? 0 : chunkWidth - remainder;
const size = required + extra;
const data = new ArrayBuffer(size);
(new Uint8Array(data)).set(this.data);
this.data = data;
}
for (let i = 0; i < count; ++i) {
results.push(this.caret++);
}
}
return results;
}
createMany(entries) {
if (entries.length > 0) {
const allocated = this.allocateMany(entries.length);
if (!this.Window) {
this.Window = this.makeWindowClass();
}
const window = new this.Window(this.data, this);
const {defaultValues} = this.schema;
for (let i = 0; i < entries.length; ++i) {
let entity;
let values = {};
if (Array.isArray(entries[i])) {
[entity, values] = entries[i];
}
else {
entity = entries[i];
}
this.map[entity] = allocated[i];
window.cursor = allocated[i] * this.constructor.width;
window.entity = entity;
for (const [i] of this.schema) {
if (i in values) {
window[i] = values[i];
}
else if ('undefined' !== typeof defaultValues[i]) {
window[i] = defaultValues[i];
}
}
}
}
}
get(entity) {
if ('undefined' === typeof this.map[entity]) {
return undefined;
}
if (!this.Window) {
this.Window = this.makeWindowClass();
}
const window = new this.Window(this.data, this);
window.cursor = this.map[entity] * this.constructor.width;
return window;
}
getUnsafe(entity) {
if ('undefined' === typeof this.map[entity]) {
return undefined;
}
if (!this.Window) {
this.Window = this.makeWindowClass();
}
if (!this.window) {
this.window = new this.Window(this.data, this);
}
this.window.cursor = this.map[entity] * this.constructor.width;
return this.window;
}
makeWindowClass() {
const Component = this;
class Window {
cursor = 0;
parent;
view;
constructor(data, parent) {
if (data) {
this.view = new DataView(data);
}
if (parent) {
this.parent = parent;
}
}
toJSON() {
const json = {};
for (const [i] of Component.schema) {
json[i] = this[i];
}
return json;
}
}
let offset = 0;
const properties = {};
const {width} = this.constructor;
const get = (type) => (
`return this.view.get${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, true);`
);
const set = (type) => [
`this.parent.setDirty(Number(this.view.getBigUint64(this.cursor + ${width - 9}, true)));`,
`this.view.set${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, v, true);`,
`this.view.setUint8(this.cursor + ${width - 1}, 1, true);`,
].join('');
/* eslint-disable no-new-func */
properties.dirty = {
get: new Function('', `return !!this.view.getUint8(this.cursor + ${width - 1}, true);`),
set: new Function('v', `this.view.setUint8(this.cursor + ${width - 1}, v ? 1 : 0, true);`),
};
properties.entity = {
get: new Function('', `return Number(this.view.getBigUint64(this.cursor + ${width - 9}, true));`),
set: new Function('v', `this.view.setBigUint64(this.cursor + ${width - 9}, BigInt(v), true);`),
};
for (const [i, spec] of this.schema) {
const {type} = spec;
properties[i] = {};
properties[i].get = new Function('', get(type));
properties[i].set = new Function('v', set(type));
offset += Schema.sizeOfType(type);
}
/* eslint-enable no-new-func */
Object.defineProperties(Window.prototype, properties);
return Window;
}
setClean() {
super.setClean();
if (!this.dirty) {
return;
}
if (!this.Window) {
this.Window = this.makeWindowClass();
}
const window = new this.Window(this.data, this);
for (let i = 0; i < this.caret; ++i) {
window.dirty = false;
window.cursor += this.constructor.width;
}
}
}

View File

@ -19,12 +19,9 @@ export default class Ecs {
constructor(Components) { constructor(Components) {
for (const i in Components) { for (const i in Components) {
const comp = new Components[i](); const comp = new Components[i]();
const {setDirty} = comp; comp.setDirty = (entity) => {
comp.setDirty = (dirty, entity) => { comp.$$dirty = true;
setDirty.call(comp, dirty); this.dirty.add(entity);
if (entity) {
this.dirty.add(entity);
}
}; };
this.Components[i] = comp; this.Components[i] = comp;
} }
@ -299,7 +296,7 @@ export default class Ecs {
tickClean() { tickClean() {
for (const i in this.Components) { for (const i in this.Components) {
this.Components[i].clean(); this.Components[i].setClean();
} }
this.dirty.clear(); this.dirty.clear();
} }

View File

@ -4,7 +4,7 @@ import Schema from './schema';
export default class Serializer { export default class Serializer {
constructor(schema) { constructor(schema) {
this.schema = schema; this.schema = schema instanceof Schema ? schema : new Schema(schema);
} }
decode(view, destination, offset = 0) { decode(view, destination, offset = 0) {

View File

@ -0,0 +1,15 @@
import {expect} from 'chai';
import Schema from '../src/schema';
it('can validate a schema', () => {
expect(() => new Schema({test: 'unknown'}))
.to.throw();
});
it('can calculate the size of an instance', () => {
expect((new Schema({foo: 'uint8', bar: 'uint32'})).sizeOf({foo: 69, bar: 420}))
.to.equal(5);
expect((new Schema({foo: 'string'})).sizeOf({foo: 'hi'}))
.to.equal(4 + 2);
});

View File

@ -0,0 +1,32 @@
import {expect} from 'chai';
import Serializer from '../src/serializer';
it('can encode and decode', () => {
const entries = [
['uint8', 255],
['int8', -128],
['int8', 127],
['uint16', 65535],
['int16', -32768],
['int16', 32767],
['uint32', 4294967295],
['int32', -2147483648],
['int32', 2147483647],
['uint64', 18446744073709551615n],
['int64', -9223372036854775808n],
['int64', 9223372036854775807n],
['float32', 0.5],
['float64', 1.234],
['string', 'hello world'],
];
const schema = entries.reduce((r, [type]) => ({...r, [Object.keys(r).length]: type}), {});
const data = entries.reduce((r, [, value]) => ({...r, [Object.keys(r).length]: value}), {});
const serializer = new Serializer(schema);
const view = new DataView(new ArrayBuffer(serializer.schema.sizeOf(data)));
serializer.encode(data, view);
const result = {};
serializer.decode(view, result);
expect(data)
.to.deep.equal(result);
});