@colyseus/schema
Version:
Binary state serializer with delta encoding for games
230 lines • 9.21 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Root = void 0;
const spec_1 = require("../encoding/spec");
const ChangeTree_1 = require("./ChangeTree");
const symbols_1 = require("../types/symbols");
class Root {
constructor(types) {
this.types = types;
this.nextUniqueId = 0;
this.refCount = {};
this.changeTrees = {};
// all changes
this.allChanges = (0, ChangeTree_1.createChangeTreeList)();
this.allFilteredChanges = (0, ChangeTree_1.createChangeTreeList)(); // TODO: do not initialize it if filters are not used
// pending changes to be encoded
this.changes = (0, ChangeTree_1.createChangeTreeList)();
this.filteredChanges = (0, ChangeTree_1.createChangeTreeList)(); // TODO: do not initialize it if filters are not used
}
getNextUniqueId() {
return this.nextUniqueId++;
}
add(changeTree) {
// Assign unique `refId` to changeTree if it doesn't have one yet.
if (changeTree.refId === undefined) {
changeTree.refId = this.getNextUniqueId();
}
const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
if (isNewChangeTree) {
this.changeTrees[changeTree.refId] = changeTree;
}
const previousRefCount = this.refCount[changeTree.refId];
if (previousRefCount === 0) {
//
// When a ChangeTree is re-added, it means that it was previously removed.
// We need to re-add all changes to the `changes` map.
//
const ops = changeTree.allChanges.operations;
let len = ops.length;
while (len--) {
changeTree.indexedOperations[ops[len]] = spec_1.OPERATION.ADD;
(0, ChangeTree_1.setOperationAtIndex)(changeTree.changes, len);
}
}
this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
// console.log("ADD", { refId: changeTree.refId, ref: changeTree.ref.constructor.name, refCount: this.refCount[changeTree.refId], isNewChangeTree });
return isNewChangeTree;
}
remove(changeTree) {
const refCount = (this.refCount[changeTree.refId]) - 1;
// console.log("REMOVE", { refId: changeTree.refId, ref: changeTree.ref.constructor.name, refCount, needRemove: refCount <= 0 });
if (refCount <= 0) {
//
// Only remove "root" reference if it's the last reference
//
changeTree.root = undefined;
delete this.changeTrees[changeTree.refId];
this.removeChangeFromChangeSet("allChanges", changeTree);
this.removeChangeFromChangeSet("changes", changeTree);
if (changeTree.filteredChanges) {
this.removeChangeFromChangeSet("allFilteredChanges", changeTree);
this.removeChangeFromChangeSet("filteredChanges", changeTree);
}
this.refCount[changeTree.refId] = 0;
changeTree.forEachChild((child, _) => {
if (child.removeParent(changeTree.ref)) {
if ((child.parentChain === undefined || // no parent, remove it
(child.parentChain && this.refCount[child.refId] > 0) // parent is still in use, but has more than one reference, remove it
)) {
this.remove(child);
}
else if (child.parentChain) {
// re-assigning a child of the same root, move it next to parent
this.moveNextToParent(child);
}
}
});
}
else {
this.refCount[changeTree.refId] = refCount;
//
// When losing a reference to an instance, it is best to move the
// ChangeTree next to its parent in the encoding queue.
//
// This way, at decoding time, the instance that contains the
// ChangeTree will be available before the ChangeTree itself. If the
// containing instance is not available, the Decoder will throw
// "refId not found" error.
//
this.recursivelyMoveNextToParent(changeTree);
}
return refCount;
}
recursivelyMoveNextToParent(changeTree) {
this.moveNextToParent(changeTree);
changeTree.forEachChild((child, _) => this.recursivelyMoveNextToParent(child));
}
moveNextToParent(changeTree) {
if (changeTree.filteredChanges) {
this.moveNextToParentInChangeTreeList("filteredChanges", changeTree);
this.moveNextToParentInChangeTreeList("allFilteredChanges", changeTree);
}
else {
this.moveNextToParentInChangeTreeList("changes", changeTree);
this.moveNextToParentInChangeTreeList("allChanges", changeTree);
}
}
moveNextToParentInChangeTreeList(changeSetName, changeTree) {
const changeSet = this[changeSetName];
const node = changeTree[changeSetName].queueRootNode;
if (!node)
return;
// Find the parent in the linked list
const parent = changeTree.parent;
if (!parent || !parent[symbols_1.$changes])
return;
const parentNode = parent[symbols_1.$changes][changeSetName]?.queueRootNode;
if (!parentNode || parentNode === node)
return;
// Use cached positions - no iteration needed!
const parentPosition = parentNode.position;
const childPosition = node.position;
// If child is already after parent, no need to move
if (childPosition > parentPosition)
return;
// Child is before parent, so we need to move it after parent
// This maintains decoding order (parent before child)
// Remove node from current position
if (node.prev) {
node.prev.next = node.next;
}
else {
changeSet.next = node.next;
}
if (node.next) {
node.next.prev = node.prev;
}
else {
changeSet.tail = node.prev;
}
// Insert node right after parent
node.prev = parentNode;
node.next = parentNode.next;
if (parentNode.next) {
parentNode.next.prev = node;
}
else {
changeSet.tail = node;
}
parentNode.next = node;
// Update positions after the move
this.updatePositionsAfterMove(changeSet, node, parentPosition + 1);
}
enqueueChangeTree(changeTree, changeSet, queueRootNode = changeTree[changeSet].queueRootNode) {
// skip
if (queueRootNode) {
return;
}
// Add to linked list if not already present
changeTree[changeSet].queueRootNode = this.addToChangeTreeList(this[changeSet], changeTree);
}
addToChangeTreeList(list, changeTree) {
const node = {
changeTree,
next: undefined,
prev: undefined,
position: list.tail ? list.tail.position + 1 : 0
};
if (!list.next) {
list.next = node;
list.tail = node;
}
else {
node.prev = list.tail;
list.tail.next = node;
list.tail = node;
}
return node;
}
updatePositionsAfterRemoval(list, removedPosition) {
// Update positions for all nodes after the removed position
let current = list.next;
let position = 0;
while (current) {
if (position >= removedPosition) {
current.position = position;
}
current = current.next;
position++;
}
}
updatePositionsAfterMove(list, node, newPosition) {
// Recalculate all positions - this is more reliable than trying to be clever
let current = list.next;
let position = 0;
while (current) {
current.position = position;
current = current.next;
position++;
}
}
removeChangeFromChangeSet(changeSetName, changeTree) {
const changeSet = this[changeSetName];
const node = changeTree[changeSetName].queueRootNode;
if (node && node.changeTree === changeTree) {
const removedPosition = node.position;
// Remove the node from the linked list
if (node.prev) {
node.prev.next = node.next;
}
else {
changeSet.next = node.next;
}
if (node.next) {
node.next.prev = node.prev;
}
else {
changeSet.tail = node.prev;
}
// Update positions for nodes that came after the removed node
this.updatePositionsAfterRemoval(changeSet, removedPosition);
// Clear ChangeTree reference
changeTree[changeSetName].queueRootNode = undefined;
return true;
}
return false;
}
}
exports.Root = Root;
//# sourceMappingURL=Root.js.map