lance-gg
Version:
A Node.js based real-time multiplayer game server
178 lines (145 loc) • 7.18 kB
text/typescript
import Utils from '../lib/Utils.js';
import BaseTypes from './BaseTypes.js';
import { NetScheme } from './NetScheme.js';
import Serializer from './Serializer.js';
type SerializableOptions = {
dataBuffer: any,
bufferOffset: number,
dry: boolean
}
type SerializedObj = {
dataBuffer: ArrayBuffer,
bufferOffset: number
}
class Serializable {
public classId: number;
constructor() {}
netScheme(): NetScheme {
return {};
}
/**
* Class can be serialized using either:
* - a class based netScheme
* - an instance based netScheme
* - completely dynamically (not implemented yet)
*
* @param {Object} serializer - Serializer instance
* @param {Object} [options] - Options object
* @param {Object} options.dataBuffer [optional] - Data buffer to write to. If null a new data buffer will be created
* @param {Number} options.bufferOffset [optional] - The buffer data offset to start writing at. Default: 0
* @param {Boolean} options.dry [optional] - Does not actually write to the buffer (useful to gather serializeable size)
* @return {Object} the serialized object. Contains attributes: dataBuffer - buffer which contains the serialized data; bufferOffset - offset where the serialized data starts.
*/
serialize(serializer: Serializer, options: SerializableOptions): SerializedObj {
options = Object.assign({
bufferOffset: 0
}, options);
let netScheme;
let dataBuffer;
let dataView;
let classId = 0;
let bufferOffset = options.bufferOffset;
let localBufferOffset = 0; // used for counting the bufferOffset
// instance classId
// TODO: when is this.classId ever populated?
if (this.classId) {
classId = this.classId;
} else {
classId = Utils.hashStr(this.constructor.name);
}
netScheme = this.netScheme();
// TODO: currently we serialize every node twice, once to calculate the size
// of the buffers and once to write them out. This can be reduced to
// a single pass by starting with a large (and static) ArrayBuffer and
// recursively building it up.
// buffer has one Uint8Array for class id, then payload
if (options.dataBuffer == null && options.dry != true) {
let bufferSize = this.serialize(serializer, { dry: true, dataBuffer: null, bufferOffset: 0 }).bufferOffset;
dataBuffer = new ArrayBuffer(bufferSize);
} else {
dataBuffer = options.dataBuffer;
}
if (options.dry != true) {
dataView = new DataView(dataBuffer);
// first set the id of the class, so that the deserializer can fetch information about it
dataView.setUint8(bufferOffset + localBufferOffset, classId);
}
// advance the offset counter
localBufferOffset += Uint8Array.BYTES_PER_ELEMENT;
if (netScheme) {
for (let property of Object.keys(netScheme).sort()) {
// write the property to buffer
if (options.dry != true) {
serializer.writeDataView(dataView, this[property], bufferOffset + localBufferOffset, netScheme[property]);
}
if (netScheme[property].type === BaseTypes.String) {
// derive the size of the string
localBufferOffset += Uint16Array.BYTES_PER_ELEMENT;
if (this[property] !== null && this[property] !== undefined)
localBufferOffset += this[property].length * Uint16Array.BYTES_PER_ELEMENT;
} else if (netScheme[property].type === BaseTypes.ClassInstance) {
// derive the size of the included class
let objectInstanceBufferOffset = this[property].serialize(serializer, { dry: true }).bufferOffset;
localBufferOffset += objectInstanceBufferOffset;
} else if (netScheme[property].type === BaseTypes.List) {
// derive the size of the list
// list starts with number of elements
localBufferOffset += Uint16Array.BYTES_PER_ELEMENT;
for (let item of this[property]) {
// todo inelegant, currently doesn't support list of lists
if (netScheme[property].itemType === BaseTypes.ClassInstance) {
let listBufferOffset = item.serialize(serializer, { dry: true }).bufferOffset;
localBufferOffset += listBufferOffset;
} else if (netScheme[property].itemType === BaseTypes.String) {
// size includes string length plus double-byte characters
localBufferOffset += Uint16Array.BYTES_PER_ELEMENT * (1 + item.length);
} else {
localBufferOffset += serializer.getTypeByteSize(netScheme[property].itemType);
}
}
} else {
// advance offset
localBufferOffset += serializer.getTypeByteSize(netScheme[property].type);
}
}
} else {
// TODO no netScheme, dynamic class
}
return { dataBuffer, bufferOffset: localBufferOffset };
}
// build a clone of this object with pruned strings (if necessary)
prunedStringsClone(serializer: Serializer, prevObject: Serializable) {
if (!prevObject) return this;
prevObject = serializer.deserialize(prevObject).obj;
// get list of string properties which changed
// @ts-ignore
let netScheme = this.netScheme();
let isString = p => netScheme[p].type === BaseTypes.String;
let hasChanged = p => prevObject[p] !== this[p];
let changedStrings = Object.keys(netScheme).filter(isString).filter(hasChanged);
if (changedStrings.length == 0) return this;
// build a clone with pruned strings
let objectClass = serializer.registeredClasses[this.classId];
let prunedCopy = new objectClass(null, { id: null });
// let prunedCopy = new this.constructor(null, { id: null });
for (let p of Object.keys(netScheme))
prunedCopy[p] = changedStrings.indexOf(p) < 0 ? this[p] : null;
return prunedCopy;
}
syncTo(other: Serializable) {
let netScheme = this.netScheme();
for (let p of Object.keys(netScheme)) {
// ignore classes and lists
if (netScheme[p].type === BaseTypes.List || netScheme[p].type === BaseTypes.ClassInstance)
continue;
// strings might be pruned
if (netScheme[p].type === BaseTypes.String) {
if (typeof other[p] === 'string') this[p] = other[p];
continue;
}
// all other values are copied
this[p] = other[p];
}
}
}
export default Serializable;