UNPKG

@dcl/ecs

Version:
305 lines (304 loc) • 16.2 kB
import { EntityState } from '../../engine/entity'; import { ReadWriteByteBuffer } from '../../serialization/ByteBuffer'; import { AppendValueOperation, CrdtMessageProtocol, DeleteComponentNetwork, DeleteEntityNetwork } from '../../serialization/crdt'; import { DeleteComponent } from '../../serialization/crdt/deleteComponent'; import { DeleteEntity } from '../../serialization/crdt/deleteEntity'; import { PutComponentOperation } from '../../serialization/crdt/putComponent'; import { CrdtMessageType } from '../../serialization/crdt/types'; import { PutNetworkComponentOperation } from '../../serialization/crdt/network/putComponentNetwork'; import { NetworkEntity as defineNetworkEntity, NetworkParent as defineNetworkParent, Transform as defineTransform } from '../../components'; import * as networkUtils from '../../serialization/crdt/network/utils'; // NetworkMessages can only have a MAX_SIZE of 12kb. So we need to send it in chunks. export const LIVEKIT_MAX_SIZE = 12; /** * @internal */ export function crdtSceneSystem(engine, onProcessEntityComponentChange) { const transports = []; // Components that we used on this system const NetworkEntity = defineNetworkEntity(engine); const NetworkParent = defineNetworkParent(engine); const Transform = defineTransform(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 ReadWriteByteBuffer(chunkMessage); let header; while ((header = CrdtMessageProtocol.getHeader(buffer))) { const offset = buffer.currentReadOffset(); let message = undefined; if (header.type === CrdtMessageType.DELETE_COMPONENT) { message = DeleteComponent.read(buffer); } else if (header.type === CrdtMessageType.DELETE_COMPONENT_NETWORK) { message = DeleteComponentNetwork.read(buffer); } else if (header.type === CrdtMessageType.PUT_COMPONENT) { message = PutComponentOperation.read(buffer); } else if (header.type === CrdtMessageType.PUT_COMPONENT_NETWORK) { message = PutNetworkComponentOperation.read(buffer); } else if (header.type === CrdtMessageType.DELETE_ENTITY) { message = DeleteEntity.read(buffer); } else if (header.type === CrdtMessageType.DELETE_ENTITY_NETWORK) { message = DeleteEntityNetwork.read(buffer); } else if (header.type === CrdtMessageType.APPEND_VALUE) { message = 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 === CrdtMessageType.DELETE_ENTITY || msg.type === CrdtMessageType.DELETE_ENTITY_NETWORK) { entitiesShouldBeCleaned.push(entityId); broadcastMessages.push(msg); } else { const entityState = engine.entityContainer.getEntityState(entityId); // Skip updates from removed entityes if (entityState === EntityState.Removed) continue; // Entities with unknown entities should update its entity state if (entityState === EntityState.Unknown) { engine.entityContainer.updateUsedEntity(entityId); } const component = engine.getComponentOrNull(msg.componentId); /* istanbul ignore else */ if (component) { if (msg.type === 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, 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 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 === CrdtMessageType.PUT_COMPONENT) { PutComponentOperation.write(message.entityId, message.timestamp, message.componentId, message.data, buffer); } else if (message.type === CrdtMessageType.DELETE_COMPONENT) { DeleteComponent.write(message.entityId, component.componentId, message.timestamp, buffer); } else if (message.type === CrdtMessageType.APPEND_VALUE) { 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 === CrdtMessageType.PUT_COMPONENT || message.type === 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.write(entityId, buffer); crdtMessages.push({ type: CrdtMessageType.DELETE_ENTITY, entityId, messageBuffer: buffer.buffer().subarray(offset, buffer.currentWriteOffset()) }); onProcessEntityComponentChange && onProcessEntityComponentChange(entityId, CrdtMessageType.DELETE_ENTITY); } // Send CRDT messages to transports const transportBuffer = new 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 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 > 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 > 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 === CrdtMessageType.PUT_COMPONENT && transformNeedsFix) { const parent = findNetworkId(NetworkParent.get(entityId)); const transformData = networkUtils.fixTransformParent(message, Transform.get(entityId), parent.entityId); const offset = buffer.currentWriteOffset(); 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 }; }