UNPKG

@dcl/ecs

Version:
332 lines (331 loc) • 17.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.crdtSceneSystem = exports.LIVEKIT_MAX_SIZE = void 0; const entity_1 = require("../../engine/entity"); const ByteBuffer_1 = require("../../serialization/ByteBuffer"); const crdt_1 = require("../../serialization/crdt"); const deleteComponent_1 = require("../../serialization/crdt/deleteComponent"); const deleteEntity_1 = require("../../serialization/crdt/deleteEntity"); const putComponent_1 = require("../../serialization/crdt/putComponent"); const types_1 = require("../../serialization/crdt/types"); const putComponentNetwork_1 = require("../../serialization/crdt/network/putComponentNetwork"); const components_1 = require("../../components"); const networkUtils = __importStar(require("../../serialization/crdt/network/utils")); // NetworkMessages can only have a MAX_SIZE of 12kb. So we need to send it in chunks. exports.LIVEKIT_MAX_SIZE = 12; /** * @internal */ function crdtSceneSystem(engine, onProcessEntityComponentChange) { const transports = []; // Components that we used on this system const NetworkEntity = (0, components_1.NetworkEntity)(engine); const NetworkParent = (0, components_1.NetworkParent)(engine); const Transform = (0, components_1.Transform)(engine); // Messages that we received at transport.onMessage waiting to be processed const receivedMessages = []; // Messages already processed by the engine but that we need to broadcast to other transports. const broadcastMessages = []; /** * * @param transportId tranport id to identiy messages * @returns a function to process received messages */ function parseChunkMessage(transportId) { /** * Receives a chunk of binary messages and stores all the valid * Component Operation Messages at messages queue * @param chunkMessage A chunk of binary messages */ return function parseChunkMessage(chunkMessage) { const buffer = new ByteBuffer_1.ReadWriteByteBuffer(chunkMessage); let header; while ((header = crdt_1.CrdtMessageProtocol.getHeader(buffer))) { const offset = buffer.currentReadOffset(); let message = undefined; if (header.type === types_1.CrdtMessageType.DELETE_COMPONENT) { message = deleteComponent_1.DeleteComponent.read(buffer); } else if (header.type === types_1.CrdtMessageType.DELETE_COMPONENT_NETWORK) { message = crdt_1.DeleteComponentNetwork.read(buffer); } else if (header.type === types_1.CrdtMessageType.PUT_COMPONENT) { message = putComponent_1.PutComponentOperation.read(buffer); } else if (header.type === types_1.CrdtMessageType.PUT_COMPONENT_NETWORK) { message = putComponentNetwork_1.PutNetworkComponentOperation.read(buffer); } else if (header.type === types_1.CrdtMessageType.DELETE_ENTITY) { message = deleteEntity_1.DeleteEntity.read(buffer); } else if (header.type === types_1.CrdtMessageType.DELETE_ENTITY_NETWORK) { message = crdt_1.DeleteEntityNetwork.read(buffer); } else if (header.type === types_1.CrdtMessageType.APPEND_VALUE) { message = crdt_1.AppendValueOperation.read(buffer); // Unknown message, we skip it } else { // consume the message buffer.incrementReadOffset(header.length); } if (message) { receivedMessages.push({ ...message, transportId, messageBuffer: buffer.buffer().subarray(offset, buffer.currentReadOffset()) }); } } }; } /** * Return and clear the messaes queue * @returns messages recieved by the transport to process on the next tick */ function getMessages(value) { const messagesToProcess = value.splice(0, value.length); return messagesToProcess; } /** * Find the local entityId associated to the network component message. * It's a mapping Network -> to Local * If it's not a network message, return the entityId received by the message */ function findNetworkId(msg) { const hasNetworkId = 'networkId' in msg; if (hasNetworkId) { for (const [entityId, network] of engine.getEntitiesWith(NetworkEntity)) { if (network.networkId === msg.networkId && network.entityId === msg.entityId) { return { entityId, network }; } } } return { entityId: msg.entityId }; } /** * This fn will be called on every tick. * Process all the messages queue received by the transport */ async function receiveMessages() { const messagesToProcess = getMessages(receivedMessages); const entitiesShouldBeCleaned = []; for (const msg of messagesToProcess) { let { entityId, network } = findNetworkId(msg); // We receive a new Entity. Create the localEntity and map it to the NetworkEntity component if (networkUtils.isNetworkMessage(msg) && !network) { entityId = engine.addEntity(); network = { entityId: msg.entityId, networkId: msg.networkId }; NetworkEntity.createOrReplace(entityId, network); } if (msg.type === types_1.CrdtMessageType.DELETE_ENTITY || msg.type === types_1.CrdtMessageType.DELETE_ENTITY_NETWORK) { entitiesShouldBeCleaned.push(entityId); broadcastMessages.push(msg); } else { const entityState = engine.entityContainer.getEntityState(entityId); // Skip updates from removed entityes if (entityState === entity_1.EntityState.Removed) continue; // Entities with unknown entities should update its entity state if (entityState === entity_1.EntityState.Unknown) { engine.entityContainer.updateUsedEntity(entityId); } const component = engine.getComponentOrNull(msg.componentId); /* istanbul ignore else */ if (component) { if (msg.type === types_1.CrdtMessageType.PUT_COMPONENT && component.componentId === Transform.componentId && NetworkEntity.has(entityId) && NetworkParent.has(entityId)) { msg.data = networkUtils.fixTransformParent(msg); } const [conflictMessage, value] = component.updateFromCrdt({ ...msg, entityId }); if (!conflictMessage) { // Add message to transport queue to be processed by others transports broadcastMessages.push(msg); onProcessEntityComponentChange && onProcessEntityComponentChange(entityId, msg.type, component, value); } } else { // TODO: test this line, it is fundammental to make the editor work broadcastMessages.push(msg); } } } // the last stage of the syncrhonization is to delete the entities for (const entity of entitiesShouldBeCleaned) { for (const definition of engine.componentsIter()) { // TODO: check this with pato/pravus definition.entityDeleted(entity, true); } engine.entityContainer.updateRemovedEntity(entity); onProcessEntityComponentChange && onProcessEntityComponentChange(entity, types_1.CrdtMessageType.DELETE_ENTITY); } } /** * Iterates the dirty map and generates crdt messages to be send */ async function sendMessages(entitiesDeletedThisTick) { // CRDT Messages will be the merge between the recieved transport messages and the new crdt messages const crdtMessages = getMessages(broadcastMessages); const buffer = new ByteBuffer_1.ReadWriteByteBuffer(); for (const component of engine.componentsIter()) { for (const message of component.getCrdtUpdates()) { const offset = buffer.currentWriteOffset(); // Avoid creating messages if there is no transport that will handle it if (transports.some((t) => t.filter(message))) { if (message.type === types_1.CrdtMessageType.PUT_COMPONENT) { putComponent_1.PutComponentOperation.write(message.entityId, message.timestamp, message.componentId, message.data, buffer); } else if (message.type === types_1.CrdtMessageType.DELETE_COMPONENT) { deleteComponent_1.DeleteComponent.write(message.entityId, component.componentId, message.timestamp, buffer); } else if (message.type === types_1.CrdtMessageType.APPEND_VALUE) { crdt_1.AppendValueOperation.write(message.entityId, message.timestamp, message.componentId, message.data, buffer); } crdtMessages.push({ ...message, messageBuffer: buffer.buffer().subarray(offset, buffer.currentWriteOffset()) }); } if (onProcessEntityComponentChange) { const rawValue = message.type === types_1.CrdtMessageType.PUT_COMPONENT || message.type === types_1.CrdtMessageType.APPEND_VALUE ? component.get(message.entityId) : undefined; onProcessEntityComponentChange(message.entityId, message.type, component, rawValue); } } } // After all updates, I execute the DeletedEntity messages for (const entityId of entitiesDeletedThisTick) { const offset = buffer.currentWriteOffset(); deleteEntity_1.DeleteEntity.write(entityId, buffer); crdtMessages.push({ type: types_1.CrdtMessageType.DELETE_ENTITY, entityId, messageBuffer: buffer.buffer().subarray(offset, buffer.currentWriteOffset()) }); onProcessEntityComponentChange && onProcessEntityComponentChange(entityId, types_1.CrdtMessageType.DELETE_ENTITY); } // Send CRDT messages to transports const transportBuffer = new ByteBuffer_1.ReadWriteByteBuffer(); for (const index in transports) { const __NetworkMessagesBuffer = []; const transportIndex = Number(index); const transport = transports[transportIndex]; const isRendererTransport = transport.type === 'renderer'; const isNetworkTransport = transport.type === 'network'; // Reset Buffer for each Transport transportBuffer.resetBuffer(); const buffer = new ByteBuffer_1.ReadWriteByteBuffer(); // Then we send all the new crdtMessages that the transport needs to process for (const message of crdtMessages) { // Check if adding this message would exceed the size limit const currentBufferSize = transportBuffer.toBinary().byteLength; const messageSize = message.messageBuffer.byteLength; if (isNetworkTransport && (currentBufferSize + messageSize) / 1024 > exports.LIVEKIT_MAX_SIZE) { // If the current buffer has content, save it as a chunk if (currentBufferSize > 0) { __NetworkMessagesBuffer.push(transportBuffer.toCopiedBinary()); transportBuffer.resetBuffer(); } // If the message itself is larger than the limit, we need to handle it specially // For now, we'll skip it to prevent infinite loops if (messageSize / 1024 > exports.LIVEKIT_MAX_SIZE) { console.error(`Message too large (${messageSize} bytes), skipping message for entity ${message.entityId}`); continue; } } // Avoid echo messages if (message.transportId === transportIndex) continue; // Redundant message for the transport if (!transport.filter(message)) continue; const { entityId } = findNetworkId(message); const transformNeedsFix = 'componentId' in message && message.componentId === Transform.componentId && Transform.has(entityId) && NetworkParent.has(entityId) && NetworkEntity.has(entityId); // If there was a LOCAL change in the transform. Add the parent to that transform if (isRendererTransport && message.type === types_1.CrdtMessageType.PUT_COMPONENT && transformNeedsFix) { const parent = findNetworkId(NetworkParent.get(entityId)); const transformData = networkUtils.fixTransformParent(message, Transform.get(entityId), parent.entityId); const offset = buffer.currentWriteOffset(); putComponent_1.PutComponentOperation.write(entityId, message.timestamp, message.componentId, transformData, buffer); transportBuffer.writeBuffer(buffer.buffer().subarray(offset, buffer.currentWriteOffset()), false); continue; } if (isRendererTransport && networkUtils.isNetworkMessage(message)) { // If it's the renderer transport and its a NetworkMessage, we need to fix the entityId field and convert it to a known Message. // PUT_NETWORK_COMPONENT -> PUT_COMPONENT let transformData = 'data' in message ? message.data : new Uint8Array(); if (transformNeedsFix) { const parent = findNetworkId(NetworkParent.get(entityId)); transformData = networkUtils.fixTransformParent(message, Transform.get(entityId), parent.entityId); } networkUtils.networkMessageToLocal({ ...message, data: transformData }, entityId, buffer, transportBuffer); // Iterate the next message continue; } // If its a network transport and its a PUT_COMPONENT that has a NetworkEntity component, we need to send this message // through comms with the EntityID and NetworkID from ther NetworkEntity so everyone can recieve this message and map to their custom entityID. if (isNetworkTransport && !networkUtils.isNetworkMessage(message)) { const networkData = NetworkEntity.getOrNull(message.entityId); // If it has networkData convert the message to PUT_NETWORK_COMPONENT. if (networkData) { networkUtils.localMessageToNetwork(message, networkData, buffer, transportBuffer); // Iterate the next message continue; } } // Common message transportBuffer.writeBuffer(message.messageBuffer, false); } if (isNetworkTransport && transportBuffer.currentWriteOffset()) { __NetworkMessagesBuffer.push(transportBuffer.toBinary()); } const message = isNetworkTransport ? __NetworkMessagesBuffer : transportBuffer.toBinary(); await transport.send(message); } } /** * @public * Add a transport to the crdt system */ function addTransport(transport) { const id = transports.push(transport) - 1; transport.onmessage = parseChunkMessage(id); } return { sendMessages, receiveMessages, addTransport }; } exports.crdtSceneSystem = crdtSceneSystem;