@ash.ts/io
Version:
Serialization and deserialization for Ash.ts - an entity component system framework for game development
468 lines (455 loc) • 16.5 kB
JavaScript
import { Signal1 } from '@ash.ts/signals';
import { Entity } from '@ash.ts/core';
class ArrayObjectCodec {
encode(object, codecManager) {
const value = [];
for (const val of object) {
const encoded = codecManager.encodeObject(val);
if (encoded) {
value.push(encoded);
}
}
return { type: 'Array', value };
}
decode(object, codecManager) {
const decoded = [];
for (const obj of object.value) {
decoded[decoded.length] = codecManager.decodeObject(obj);
}
return decoded;
}
decodeIntoObject(target, object, codecManager) {
for (const obj of object.value) {
target[target.length] = codecManager.decodeObject(obj);
}
}
decodeIntoProperty(parent, property, object, codecManager) {
this.decodeIntoObject(parent[property], object, codecManager);
}
}
class ClassObjectCodec {
encode(object, codecManager) {
return { type: 'Class', value: codecManager.classToStringMap.get(object) };
}
decode(object, codecManager) {
return codecManager.stringToClassMap.get(object.value) || null;
}
decodeIntoObject(target, object, codecManager) {
throw new Error('Can\'t decode into a native object because the object is passed by value, not by reference, so we\'re decoding into a local copy not the original.');
}
decodeIntoProperty(parent, property, object, codecManager) {
this.decodeIntoObject(parent[property], object, codecManager);
}
}
class NativeObjectCodec {
encode(object, codecManager) {
return { type: typeof object, value: object };
}
decode(object, codecManager) {
return object.value;
}
decodeIntoObject(target, object, codecManager) {
throw new Error('Can\'t decode into a native object because the object is passed by value, not by reference,'
+ 'so we\'re decoding into a local copy not the original.');
}
decodeIntoProperty(parent, property, object, codecManager) {
parent[property] = object.value;
}
}
class ObjectReflection {
constructor(component, type) {
this._propertyTypes = new Map();
this._type = type || component.constructor.name;
const { _propertyTypes } = this;
const filter = (descs) => (key) => {
const desc = descs[key];
return !!desc.enumerable || (!!desc.get && !!desc.set);
};
const ownKeys = Object.getOwnPropertyDescriptors(component);
const protoDescriptor = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(component));
let keys = Object.keys(ownKeys).filter(filter(ownKeys));
keys = keys.concat(Object.keys(protoDescriptor).filter(filter(protoDescriptor)));
for (const key of keys) {
const cmp = component[key];
const name = typeof cmp;
switch (name.toLowerCase()) {
case 'object':
if (cmp === null) {
_propertyTypes.set(key, 'null');
}
else {
_propertyTypes.set(key, cmp.constructor.name);
}
break;
case 'undefined':
case 'null':
break;
default:
_propertyTypes.set(key, name);
}
}
const types = component.constructor.__ash_types__;
if (!types)
return;
for (const [key, componentType] of types) {
const { name } = componentType;
if (!_propertyTypes.has(key)) {
_propertyTypes.set(key, name);
}
}
}
get propertyTypes() {
return this._propertyTypes;
}
get type() {
return this._type;
}
}
class ObjectReflectionFactory {
static reflection(component) {
const type = component.constructor.prototype.constructor;
const { reflections, classMap } = ObjectReflectionFactory;
if (!reflections.has(type) && classMap.has(type)) {
reflections.set(type, new ObjectReflection(component, classMap.get(type)));
}
return reflections.get(type) || null;
}
}
ObjectReflectionFactory.classMap = new Map();
ObjectReflectionFactory.reflections = new Map();
class ReflectionObjectCodec {
encode(object, codecManager) {
const reflection = ObjectReflectionFactory.reflection(object);
if (!reflection)
return null;
const properties = {};
const keys = reflection.propertyTypes.keys();
for (const name of keys) {
properties[name] = codecManager.encodeObject(object[name]);
}
return { type: reflection.type, value: properties };
}
decode(object, codecManager) {
const Type = codecManager.stringToClassMap.get(object.type) || null;
if (!Type) {
return null;
}
const decoded = new Type();
const keys = Object.keys(object.value);
for (const name of keys) {
decoded[name] = codecManager.decodeObject(object.value[name]);
}
return decoded;
}
decodeIntoObject(target, object, codecManager) {
const keys = Object.keys(object.value);
for (const name of keys) {
if (target[name]) {
codecManager.decodeIntoProperty(target, name, object.value[name]);
}
else {
target[name] = codecManager.decodeObject(object.value[name]);
}
}
}
decodeIntoProperty(parent, property, object, codecManager) {
this.decodeIntoObject(parent[property], object, codecManager);
}
}
class CodecManager {
constructor(classMap) {
classMap.set('number', Number);
classMap.set('string', String);
classMap.set('boolean', Boolean);
this.stringToClassMap = classMap;
const classToStringMap = new Map();
for (const [className, classType] of classMap) {
classToStringMap.set(classType, className);
}
ObjectReflectionFactory.classMap = classToStringMap;
this.classToStringMap = classToStringMap;
this.codecs = new Map();
this.reflectionCodec = new ReflectionObjectCodec();
const nativeCodec = new NativeObjectCodec();
this.addCustomCodec(nativeCodec, Number);
this.addCustomCodec(nativeCodec, String);
this.addCustomCodec(nativeCodec, Boolean);
this.addCustomCodec(new ClassObjectCodec(), Function);
this.addCustomCodec(new ArrayObjectCodec(), Array);
}
getCodecForObject(object) {
const nativeTypes = {
number: Number,
string: String,
boolean: Boolean,
};
let type = nativeTypes[typeof object];
if (!type && object instanceof Array)
type = Array;
if (!type && object instanceof Object)
type = object.constructor;
if (this.codecs.has(type)) {
return this.codecs.get(type);
}
return null;
}
getCodecForType(type) {
if (this.codecs.has(type)) {
return this.codecs.get(type);
}
return null;
}
getCodecForComponent(component) {
const codec = this.getCodecForObject(component);
if (codec === null) {
return this.reflectionCodec;
}
return codec;
}
getCodecForComponentType(type) {
const codec = this.getCodecForType(type);
if (codec === null) {
return this.reflectionCodec;
}
return codec;
}
addCustomCodec(codec, type) {
this.codecs.set(type, codec);
}
encodeComponent(object) {
const codec = this.getCodecForComponent(object);
if (codec) {
return codec.encode(object, this);
}
return null;
}
encodeObject(object) {
if (object === null) {
return { type: 'null', value: null };
}
const codec = this.getCodecForObject(object);
if (codec) {
return codec.encode(object, this);
}
return { type: 'null', value: null };
}
decodeComponent(object) {
if (!object.type || object.value === null) {
return null;
}
const type = this.stringToClassMap.get(object.type);
const codec = type ? this.getCodecForComponentType(type) : null;
if (codec) {
return codec.decode(object, this);
}
return null;
}
decodeObject(object) {
if (!object.type || object.value === null) {
return null;
}
const type = this.stringToClassMap.get(object.type);
const codec = type ? this.getCodecForType(type) : null;
if (codec) {
return codec.decode(object, this);
}
return null;
}
decodeIntoComponent(target, encoded) {
if (!encoded.type || encoded.value === null) {
return;
}
const type = this.stringToClassMap.get(encoded.type);
const codec = type ? this.getCodecForComponentType(type) : null;
if (codec) {
codec.decodeIntoObject(target, encoded, this);
}
}
decodeIntoProperty(parent, property, encoded) {
if (!encoded.type || encoded.value === null) {
return;
}
const type = this.stringToClassMap.get(encoded.type);
const codec = type ? this.getCodecForType(type) : null;
if (codec) {
codec.decodeIntoProperty(parent, property, encoded, this);
}
}
}
class EngineDecoder {
constructor(codecManager) {
this.codecManager = codecManager;
this.componentMap = [];
this.encodedComponentMap = [];
}
reset() {
this.componentMap.length = 0;
this.encodedComponentMap.length = 0;
}
decodeEngine(encodedData, engine) {
for (const encodedComponent of encodedData.components) {
this.decodeComponent(encodedComponent);
}
for (const encodedEntity of encodedData.entities) {
engine.addEntity(this.decodeEntity(encodedEntity));
}
}
decodeOverEngine(encodedData, engine) {
for (const encodedComponent of encodedData.components) {
this.encodedComponentMap[encodedComponent.id] = encodedComponent;
this.decodeComponent(encodedComponent);
}
for (const encodedEntity of encodedData.entities) {
if (encodedEntity.name) {
const { name } = encodedEntity;
if (name) {
const existingEntity = engine.getEntityByName(name);
if (existingEntity) {
this.overlayEntity(existingEntity, encodedEntity);
continue;
}
}
}
engine.addEntity(this.decodeEntity(encodedEntity));
}
}
overlayEntity(entity, encodedEntity) {
for (const componentId of encodedEntity.components) {
if (this.componentMap[componentId]) {
const newComponent = this.componentMap[componentId];
if (newComponent) {
const type = newComponent.constructor.prototype.constructor;
const existingComponent = entity.get(type);
if (existingComponent) {
this.codecManager.decodeIntoComponent(existingComponent, this.encodedComponentMap[componentId]);
}
else {
entity.add(newComponent);
}
}
}
}
}
decodeEntity(encodedEntity) {
const entity = new Entity();
if (encodedEntity.name) {
entity.name = encodedEntity.name;
}
for (const componentId of encodedEntity.components) {
if (this.componentMap[componentId]) {
entity.add(this.componentMap[componentId]);
}
}
return entity;
}
decodeComponent(encodedComponent) {
const type = this.codecManager.stringToClassMap.get(encodedComponent.type);
if (!type)
return;
const codec = this.codecManager.getCodecForComponent(type);
if (!codec)
return;
const decodedComponent = this.codecManager.decodeComponent(encodedComponent);
if (decodedComponent)
this.componentMap[encodedComponent.id] = decodedComponent;
}
}
class EngineEncoder {
constructor(codecManager) {
this.codecManager = codecManager;
this.reset();
}
reset() {
this.nextComponentId = 1;
this.encodedEntities = [];
this.encodedComponents = [];
this.componentEncodingMap = new Map();
this.encoded = { entities: this.encodedEntities, components: this.encodedComponents };
}
encodeEngine(engine) {
for (const entity of engine.entities) {
this.encodeEntity(entity);
}
return this.encoded;
}
encodeEntity(entity) {
const components = entity.getAll();
const componentIds = [];
for (const component of components) {
const encodedComponentId = this.encodeComponent(component);
if (encodedComponentId > -1) {
componentIds.push(encodedComponentId);
}
}
this.encodedEntities.push({
name: entity.name,
components: componentIds,
});
}
encodeComponent(component) {
if (this.componentEncodingMap.has(component)) {
return this.componentEncodingMap.get(component).id;
}
const encodedObject = this.codecManager.encodeComponent(component);
if (encodedObject) {
const encoded = encodedObject;
this.nextComponentId += 1;
encoded.id = this.nextComponentId;
this.componentEncodingMap.set(component, encoded);
this.encodedComponents.push(encoded);
return encoded.id;
}
return -1;
}
}
class ObjectEngineCodec {
constructor(classMap) {
this.encodeCompleteSignal = new Signal1();
this.decodeCompleteSignal = new Signal1();
this.codecManager = new CodecManager(classMap);
this.encoder = new EngineEncoder(this.codecManager);
this.decoder = new EngineDecoder(this.codecManager);
}
addCustomCodec(codec, ...types) {
for (const type of types) {
this.codecManager.addCustomCodec(codec, type);
}
}
encodeEngine(engine) {
this.encoder.reset();
const encoded = this.encoder.encodeEngine(engine);
this.encodeCompleteSignal.dispatch(encoded);
return encoded;
}
decodeEngine(encodedData, engine) {
this.decoder.reset();
this.decoder.decodeEngine(encodedData, engine);
this.decodeCompleteSignal.dispatch(engine);
}
decodeOverEngine(encodedData, engine) {
this.decoder.reset();
this.decoder.decodeOverEngine(encodedData, engine);
this.decodeCompleteSignal.dispatch(engine);
}
get encodeComplete() {
return this.encodeCompleteSignal;
}
get decodeComplete() {
return this.decodeCompleteSignal;
}
}
class JsonEngineCodec extends ObjectEngineCodec {
encodeEngine(engine) {
const object = super.encodeEngine(engine);
return JSON.stringify(object);
}
decodeEngine(encodedData, engine) {
const object = JSON.parse(encodedData);
super.decodeEngine(object, engine);
}
decodeOverEngine(encodedData, engine) {
const object = JSON.parse(encodedData);
super.decodeOverEngine(object, engine);
}
}
export { CodecManager, JsonEngineCodec, ObjectEngineCodec };