UNPKG

@tamgl/colyseus-schema

Version:

Binary state serializer with delta encoding for games

378 lines (320 loc) 11.2 kB
import { OPERATION } from "../encoding/spec"; import { Metadata } from "../Metadata"; import { Schema } from "../Schema"; import type { Ref } from "../encoder/ChangeTree"; import type { Decoder } from "./Decoder"; import { Iterator, decode } from "../encoding/decode"; import { $childType, $deleteByIndex, $getByIndex } from "../types/symbols"; import type { MapSchema } from "../types/custom/MapSchema"; import type { ArraySchema } from "../types/custom/ArraySchema"; import type { CollectionSchema } from "../types/custom/CollectionSchema"; import { getType } from "../types/registry"; import { Collection } from "../types/HelperTypes"; export interface DataChange<T = any, F = string> { ref: Ref, refId: number, op: OPERATION, field: F; dynamicIndex?: number | string; value: T; previousValue: T; } export const DEFINITION_MISMATCH = -1; export type DecodeOperation<T extends Schema = any> = ( decoder: Decoder<T>, bytes: Buffer, it: Iterator, ref: Ref, allChanges: DataChange[], ) => number | void; export function decodeValue( decoder: Decoder, operation: OPERATION, ref: Ref, index: number, type: any, bytes: Buffer, it: Iterator, allChanges: DataChange[], ) { const $root = decoder.root; const previousValue = ref[$getByIndex](index); let value: any; if ((operation & OPERATION.DELETE) === OPERATION.DELETE) { // Flag `refId` for garbage collection. const previousRefId = $root.refIds.get(previousValue); if (previousRefId !== undefined) { $root.removeRef(previousRefId); } // // Delete operations // if (operation !== OPERATION.DELETE_AND_ADD) { ref[$deleteByIndex](index); } value = undefined; } if (operation === OPERATION.DELETE) { // // Don't do anything // } else if (Schema.is(type)) { const refId = decode.number(bytes, it); value = $root.refs.get(refId); if ((operation & OPERATION.ADD) === OPERATION.ADD) { const childType = decoder.getInstanceType(bytes, it, type); if (!value) { value = decoder.createInstanceOfType(childType); } $root.addRef( refId, value, ( value !== previousValue || // increment ref count if value has changed (operation === OPERATION.DELETE_AND_ADD && value === previousValue) // increment ref count if the same instance is being added again ) ); } } else if (typeof(type) === "string") { // // primitive value (number, string, boolean, etc) // value = decode[type as string](bytes, it); } else { const typeDef = getType(Object.keys(type)[0]); const refId = decode.number(bytes, it); const valueRef: Ref = ($root.refs.has(refId)) ? previousValue || $root.refs.get(refId) : new typeDef.constructor(); value = valueRef.clone(true); value[$childType] = Object.values(type)[0]; // cache childType for ArraySchema and MapSchema if (previousValue) { let previousRefId = $root.refIds.get(previousValue); if (previousRefId !== undefined && refId !== previousRefId) { // // enqueue onRemove if structure has been replaced. // const entries: IterableIterator<[any, any]> = previousValue.entries(); let iter: IteratorResult<[any, any]>; while ((iter = entries.next()) && !iter.done) { const [key, value] = iter.value; // if value is a schema, remove its reference if (typeof(value) === "object") { previousRefId = $root.refIds.get(value); $root.removeRef(previousRefId); } allChanges.push({ ref: previousValue, refId: previousRefId, op: OPERATION.DELETE, field: key, value: undefined, previousValue: value, }); } } } $root.addRef(refId, value, ( valueRef !== previousValue || (operation === OPERATION.DELETE_AND_ADD && valueRef === previousValue) )); } return { value, previousValue }; } export const decodeSchemaOperation: DecodeOperation = function ( decoder: Decoder<any>, bytes: Buffer, it: Iterator, ref: Ref, allChanges: DataChange[], ) { const first_byte = bytes[it.offset++]; const metadata: Metadata = ref.constructor[Symbol.metadata]; // "compressed" index + operation const operation = (first_byte >> 6) << 6 const index = first_byte % (operation || 255); // skip early if field is not defined const field = metadata[index]; if (field === undefined) { console.warn("@colyseus/schema: field not defined at", { index, ref: ref.constructor.name, metadata }); return DEFINITION_MISMATCH; } const { value, previousValue } = decodeValue( decoder, operation, ref, index, field.type, bytes, it, allChanges, ); if (value !== null && value !== undefined) { ref[field.name] = value; } // add change if (previousValue !== value) { allChanges.push({ ref, refId: decoder.currentRefId, op: operation, field: field.name, value, previousValue, }); } } export const decodeKeyValueOperation: DecodeOperation = function ( decoder: Decoder<any>, bytes: Buffer, it: Iterator, ref: Ref, allChanges: DataChange[] ) { // "uncompressed" index + operation (array/map items) const operation = bytes[it.offset++]; if (operation === OPERATION.CLEAR) { // // When decoding: // - enqueue items for DELETE callback. // - flag child items for garbage collection. // decoder.removeChildRefs(ref as unknown as Collection, allChanges); (ref as any).clear(); return; } const index = decode.number(bytes, it); const type = ref[$childType]; let dynamicIndex: number | string; if ((operation & OPERATION.ADD) === OPERATION.ADD) { // ADD or DELETE_AND_ADD if (typeof(ref['set']) === "function") { dynamicIndex = decode.string(bytes, it); // MapSchema ref['setIndex'](index, dynamicIndex); } else { dynamicIndex = index; // ArraySchema } } else { // get dynamic index from "ref" dynamicIndex = ref['getIndex'](index); } const { value, previousValue } = decodeValue( decoder, operation, ref, index, type, bytes, it, allChanges, ); if (value !== null && value !== undefined) { if (typeof(ref['set']) === "function") { // MapSchema (ref as MapSchema)['$items'].set(dynamicIndex as string, value); } else if (typeof(ref['$setAt']) === "function") { // ArraySchema (ref as ArraySchema)['$setAt'](index, value, operation); } else if (typeof(ref['add']) === "function") { // CollectionSchema && SetSchema const index = (ref as CollectionSchema).add(value); if (typeof(index) === "number") { ref['setIndex'](index, index); } } } // add change if (previousValue !== value) { allChanges.push({ ref, refId: decoder.currentRefId, op: operation, field: "", // FIXME: remove this dynamicIndex, value, previousValue, }); } } export const decodeArray: DecodeOperation = function ( decoder: Decoder<any>, bytes: Buffer, it: Iterator, ref: ArraySchema, allChanges: DataChange[] ) { // "uncompressed" index + operation (array/map items) let operation = bytes[it.offset++]; let index: number; if (operation === OPERATION.CLEAR) { // // When decoding: // - enqueue items for DELETE callback. // - flag child items for garbage collection. // decoder.removeChildRefs(ref as unknown as Collection, allChanges); (ref as ArraySchema).clear(); return; } else if (operation === OPERATION.REVERSE) { (ref as ArraySchema).reverse(); return; } else if (operation === OPERATION.DELETE_BY_REFID) { // TODO: refactor here, try to follow same flow as below const refId = decode.number(bytes, it); const previousValue = decoder.root.refs.get(refId); index = ref.findIndex((value) => value === previousValue); ref[$deleteByIndex](index); allChanges.push({ ref, refId: decoder.currentRefId, op: OPERATION.DELETE, field: "", // FIXME: remove this dynamicIndex: index, value: undefined, previousValue, }); return; } else if (operation === OPERATION.ADD_BY_REFID) { const refId = decode.number(bytes, it); const itemByRefId = decoder.root.refs.get(refId); // if item already exists, use existing index if (itemByRefId) { index = ref.findIndex((value) => value === itemByRefId); } // fallback to use last index if (index === -1 || index === undefined) { index = ref.length; } } else { index = decode.number(bytes, it); } const type = ref[$childType]; let dynamicIndex: number | string = index; const { value, previousValue } = decodeValue( decoder, operation, ref, index, type, bytes, it, allChanges, ); if ( value !== null && value !== undefined && value !== previousValue // avoid setting same value twice (if index === 0 it will result in a "unshift" for ArraySchema) ) { // ArraySchema (ref as ArraySchema)['$setAt'](index, value, operation); } // add change if (previousValue !== value) { allChanges.push({ ref, refId: decoder.currentRefId, op: operation, field: "", // FIXME: remove this dynamicIndex, value, previousValue, }); } }