From 336e4225a2f10b1b096bdf279097b4b1fe9e3078 Mon Sep 17 00:00:00 2001 From: Gonzalo DCL Date: Tue, 21 Nov 2023 17:37:30 -0300 Subject: [PATCH] wip sync parent entities --- packages/@dcl/ecs/src/components/index.ts | 13 +++- .../src/components/manual/NetworkParent.ts | 20 ++++++ packages/@dcl/ecs/src/components/types.ts | 1 + packages/@dcl/ecs/src/index.ts | 8 ++- .../src/serialization/crdt/network/utils.ts | 38 +++++++++++- packages/@dcl/ecs/src/systems/crdt/index.ts | 61 +++++++++++++++---- packages/@dcl/sdk/src/network/README.md | 29 +++++---- packages/@dcl/sdk/src/network/parent.ts | 37 +++++++++++ packages/@dcl/sdk/src/network/sync-entity.ts | 21 ++++--- 9 files changed, 188 insertions(+), 40 deletions(-) create mode 100644 packages/@dcl/ecs/src/components/manual/NetworkParent.ts create mode 100644 packages/@dcl/sdk/src/network/parent.ts diff --git a/packages/@dcl/ecs/src/components/index.ts b/packages/@dcl/ecs/src/components/index.ts index e4614dd4d..25cccea7f 100644 --- a/packages/@dcl/ecs/src/components/index.ts +++ b/packages/@dcl/ecs/src/components/index.ts @@ -8,7 +8,8 @@ import { defineTweenComponent, TweenComponentDefinitionExtended } from './extend import { LwwComponentGetter, GSetComponentGetter } from './generated/index.gen' import defineNameComponent, { NameType } from './manual/Name' import defineSyncComponent, { ISyncComponentsType } from './manual/SyncComponents' -import defineEntityNetwork, { INetowrkEntityType } from './manual/NetworkEntity' +import defineNetworkEntity, { INetowrkEntityType } from './manual/NetworkEntity' +import defineNetworkParent, { INetowrkParentType } from './manual/NetworkParent' import { defineTransformComponent, TransformComponentExtended } from './manual/Transform' export * from './generated/index.gen' @@ -64,4 +65,12 @@ export const SyncComponents: ( /* @__PURE__ */ export const NetworkEntity: ( engine: Pick -) => LastWriteWinElementSetComponentDefinition = (engine) => defineEntityNetwork(engine) +) => LastWriteWinElementSetComponentDefinition = (engine) => defineNetworkEntity(engine) + +/** + * @alpha + */ +/* @__PURE__ */ +export const NetworkParent: ( + engine: Pick +) => LastWriteWinElementSetComponentDefinition = (engine) => defineNetworkParent(engine) diff --git a/packages/@dcl/ecs/src/components/manual/NetworkParent.ts b/packages/@dcl/ecs/src/components/manual/NetworkParent.ts new file mode 100644 index 000000000..182df32f8 --- /dev/null +++ b/packages/@dcl/ecs/src/components/manual/NetworkParent.ts @@ -0,0 +1,20 @@ +import { Entity } from '../../engine' +import { IEngine, LastWriteWinElementSetComponentDefinition } from '../../engine/types' +import { Schemas } from '../../schemas' + +export interface INetowrkParentType { + networkId: number + entityId: Entity +} + +export type INetowrkParent = LastWriteWinElementSetComponentDefinition + +function defineNetworkParentComponent(engine: Pick) { + const EntityNetwork = engine.defineComponent('core-schema::Network-Parent', { + networkId: Schemas.Int64, + entityId: Schemas.Entity + }) + return EntityNetwork +} + +export default defineNetworkParentComponent diff --git a/packages/@dcl/ecs/src/components/types.ts b/packages/@dcl/ecs/src/components/types.ts index 317cf8a6c..a9c7b3a9c 100644 --- a/packages/@dcl/ecs/src/components/types.ts +++ b/packages/@dcl/ecs/src/components/types.ts @@ -7,3 +7,4 @@ export type { TransformComponentExtended, TransformTypeWithOptionals } from './m export type { NameComponent, NameType } from './manual/Name' export type { ISyncComponents, ISyncComponentsType } from './manual/SyncComponents' export type { INetowrkEntity, INetowrkEntityType } from './manual/NetworkEntity' +export type { INetowrkParent, INetowrkParentType } from './manual/NetworkParent' diff --git a/packages/@dcl/ecs/src/index.ts b/packages/@dcl/ecs/src/index.ts index db57d882e..5fe9f841d 100644 --- a/packages/@dcl/ecs/src/index.ts +++ b/packages/@dcl/ecs/src/index.ts @@ -27,7 +27,8 @@ import { AnimatorComponentDefinitionExtended, ISyncComponents, TweenComponentDefinitionExtended, - INetowrkEntity + INetowrkEntity, + INetowrkParent } from './components/types' import { NameComponent } from './components/manual/Name' @@ -50,6 +51,11 @@ export const SyncComponents: ISyncComponents = /* @__PURE__*/ components.SyncCom * Tag a entity to be syncronized through comms */ export const NetworkEntity: INetowrkEntity = /* @__PURE__*/ components.NetworkEntity(engine) +/** + * @alpha + * Tag a entity to be syncronized through comms + */ +export const NetworkParent: INetowrkParent = /* @__PURE__*/ components.NetworkParent(engine) // export components for global engine export * from './components/generated/global.gen' diff --git a/packages/@dcl/ecs/src/serialization/crdt/network/utils.ts b/packages/@dcl/ecs/src/serialization/crdt/network/utils.ts index deaa63770..685b8ad84 100644 --- a/packages/@dcl/ecs/src/serialization/crdt/network/utils.ts +++ b/packages/@dcl/ecs/src/serialization/crdt/network/utils.ts @@ -1,15 +1,16 @@ import { Entity } from '../../../engine' -import { ReceiveMessage } from '../../../runtime/types' +import { ReceiveMessage, TransformType } from '../../../runtime/types' import { ReceiveNetworkMessage } from '../../../systems/crdt/types' -import { ByteBuffer } from '../../ByteBuffer' +import { ByteBuffer, ReadWriteByteBuffer } from '../../ByteBuffer' import { PutComponentOperation } from '../putComponent' -import { CrdtMessageType } from '../types' +import { CrdtMessageType, PutComponentMessageBody } from '../types' import { DeleteComponent } from '../deleteComponent' import { DeleteEntity } from '../deleteEntity' import { INetowrkEntityType } from '../../../components/types' import { PutNetworkComponentOperation } from './putComponentNetwork' import { DeleteComponentNetwork } from './deleteComponentNetwork' import { DeleteEntityNetwork } from './deleteEntityNetwork' +import { TransformSchema } from '../../../components/manual/Transform' export function isNetworkMessage(message: ReceiveMessage): message is ReceiveNetworkMessage { return [ @@ -59,3 +60,34 @@ export function localMessageToNetwork( } destinationBuffer.writeBuffer(buffer.buffer().subarray(offset, buffer.currentWriteOffset()), false) } + +export function addTransformParentToRenderer( + message: PutComponentMessageBody, + transfrom: TransformType, + parentEntityId: Entity, + localBuffer: ByteBuffer, + destinationBuffer: ByteBuffer +) { + // Generate new transform raw data with the parent + transfrom.parent = parentEntityId + TransformSchema.serialize(transfrom, localBuffer) + const data = localBuffer.toBinary() + localBuffer.resetBuffer() + + // Write the message in the destination buffer (renderer transport) + const offset = localBuffer.currentWriteOffset() + PutComponentOperation.write(message.entityId, message.timestamp, message.componentId, data, localBuffer) + destinationBuffer.writeBuffer(localBuffer.buffer().subarray(offset, localBuffer.currentWriteOffset()), false) +} + +export function removeTransformParentFromRenderer(message: PutComponentMessageBody) { + const buffer = new ReadWriteByteBuffer() + buffer.writeBuffer(message.data) + const transform = TransformSchema.deserialize(buffer) + // Generate new transform raw data with the parent + transform.parent = undefined + + buffer.resetBuffer() + TransformSchema.serialize(transform, buffer) + return buffer.toBinary() +} diff --git a/packages/@dcl/ecs/src/systems/crdt/index.ts b/packages/@dcl/ecs/src/systems/crdt/index.ts index 7817530a2..5496d026c 100644 --- a/packages/@dcl/ecs/src/systems/crdt/index.ts +++ b/packages/@dcl/ecs/src/systems/crdt/index.ts @@ -14,7 +14,11 @@ import { PutComponentOperation } from '../../serialization/crdt/putComponent' import { CrdtMessageType, CrdtMessageHeader, CrdtMessage } from '../../serialization/crdt/types' import { ReceiveMessage, Transport } from './types' import { PutNetworkComponentOperation } from '../../serialization/crdt/network/putComponentNetwork' -import { NetworkEntity as defineNetworkEntity } from '../../components' +import { + NetworkEntity as defineNetworkEntity, + NetworkParent as defineNetworkParent, + Transform as defineTransform +} from '../../components' import { INetowrkEntityType } from '../../components/types' import * as networkUtils from '../../serialization/crdt/network/utils' @@ -33,7 +37,12 @@ export type OnChangeFunction = ( */ export function crdtSceneSystem(engine: PreEngine, onProcessEntityComponentChange: OnChangeFunction | null) { const transports: Transport[] = [] + + // 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: ReceiveMessage[] = [] // Messages already processed by the engine but that we need to broadcast to other transports. @@ -101,16 +110,20 @@ export function crdtSceneSystem(engine: PreEngine, onProcessEntityComponentChang * It's a mapping Network -> to Local * If it's not a network message, return the entityId received by the message */ - function findNetworkId(msg: ReceiveMessage): { entityId: Entity; network?: INetowrkEntityType } { - if (!networkUtils.isNetworkMessage(msg)) { - return { entityId: msg.entityId } - } + function findNetworkId(msg: { entityId: Entity; networkId?: number }): { + entityId: Entity + network?: INetowrkEntityType + } { + const hasNetworkId = 'networkId' in msg - for (const [entityId, network] of engine.getEntitiesWith(NetworkEntity)) { - if (network.networkId === msg.networkId && network.entityId === msg.entityId) { - return { entityId, network } + 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 } } @@ -128,7 +141,8 @@ export function crdtSceneSystem(engine: PreEngine, onProcessEntityComponentChang // We receive a new Entity. Create the localEntity and map it to the NetworkEntity component if (networkUtils.isNetworkMessage(msg) && !network) { entityId = engine.addEntity() - NetworkEntity.createOrReplace(entityId, { entityId: msg.entityId, networkId: msg.networkId }) + 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) @@ -148,8 +162,15 @@ export function crdtSceneSystem(engine: PreEngine, onProcessEntityComponentChang /* istanbul ignore else */ if (component) { + if ( + msg.type === CrdtMessageType.PUT_COMPONENT && + component.componentId === Transform.componentId && + NetworkEntity.has(entityId) && + NetworkParent.has(entityId) + ) { + msg.data = networkUtils.removeTransformParentFromRenderer(msg) + } const [conflictMessage, value] = component.updateFromCrdt({ ...msg, entityId }) - if (!conflictMessage) { // Add message to transport queue to be processed by others transports broadcastMessages.push(msg) @@ -239,9 +260,25 @@ export function crdtSceneSystem(engine: PreEngine, onProcessEntityComponentChang // Redundant message for the transport if (!transport.filter(message)) continue - // 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 + // Fix network parenting issue for the renderer + if ( + isRendererTransport && + message.type === CrdtMessageType.PUT_COMPONENT && + message.componentId === Transform.componentId && + Transform.has(message.entityId) && + NetworkParent.has(message.entityId) && + NetworkEntity.has(message.entityId) + ) { + const transform = Transform.get(message.entityId) + const networkParent = NetworkParent.get(message.entityId) + const parent = findNetworkId(networkParent) + networkUtils.addTransformParentToRenderer(message, transform, parent.entityId, buffer, transportBuffer) + 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 const { entityId } = findNetworkId(message) networkUtils.networkMessageToLocal(message, entityId, buffer, transportBuffer) // Iterate the next message diff --git a/packages/@dcl/sdk/src/network/README.md b/packages/@dcl/sdk/src/network/README.md index 99e5c7f69..f179bbbdd 100644 --- a/packages/@dcl/sdk/src/network/README.md +++ b/packages/@dcl/sdk/src/network/README.md @@ -74,21 +74,20 @@ B creates sync entity 514 (child) Transform.create({ parent, position: {} }) syncEntity(child, [Transform.componentId], SyncEntities.ChildDoor) -So now client A has different raw data for the Transform with client B, because they have different parents. +So now client A & B had different raw data for the Transform component, because they have different parents. Meaning that we have an inconsistent CRDT State between two clients. So if there is a new message comming from client C we could have conflicts for client A but maybe not for client B. -I want to cry. Solution 1: - What if we introduce a new ParentSync. - This ParentSync will be in charge of syncronizing the parenting. If we have ParentSync then we should always ignore the Transform.parent property. - The parentSync will have both entityId and networkId such as the PutNetworkMessage so we can map the entity in every client. - ParentSync.Schema = { parent: Entity; networkId: number }. + What if we introduce a new ParentNetwork component. + This ParentNetwork will be in charge of syncronizing the parenting. If we have ParentNetwork then we should always ignore the Transform.parent property. + The ParentNetwork will have both entityId and networkId such as the PutNetworkMessage so we can map the entity in every client. + ParentNetwork.Schema = { parent: Entity; networkId: number }. Being the networkId the id of the user that owns that parent entity. const newParent: Entity = someEntityWeWantToUpdate() // 512 this new parent returns the entity of this client, but it may not be the client who owns this entity. const networkParent = NetworkEntity.getOrNull(newParent) // { entityId: 1025, networkId: random } - ParentSync.create(child, networkParent) + ParentNetwork.create(child, networkParent) Now imagine that client A want to create a new parent for a child that originally was created by Client B ```ts @@ -96,25 +95,25 @@ Solution 1: Transform.create(parent, { position: somePosition }) syncEntity(parent, Transform.componentId) const childEntity: Entity = someEntityWeWantToUpdate() - ParentSync.createOrReplace(childEntity, { parent, networkId: 'clientA' }) + ParentNetwork.createOrReplace(childEntity, { parent, networkId: 'clientA' }) ``` This will generate two PUT_NETWORK_COMPONENT messages. One for the parent entity with the transform component ( networkId: clientA, entityId: parent ) - And another for the ParentSync of the child entity that was originally created by client B (networkId: clientB, entityId: child). - Every client will know how to map this entity because the ParentSync has the pointers to the parent entity. - ParentSync will point to the entity that was created on the first message. + And another for the ParentNetwork of the child entity that was originally created by client B (networkId: clientB, entityId: child). + Every client will know how to map this entity because the ParentNetwork has the pointers to the parent entity. + ParentNetwork will point to the entity that was created on the first message. But we need to fix the parenting for the renderer, so it doesnt know about this logic - So every time we send a Transform component to the renderer, we should update the transform.parent property with the mapped Entity that we fetch from the ParentSync. - if (isTransform(message) && isRendererTransport && ParentSync.getOrNull(message.entityId)) { + So every time we send a Transform component to the renderer, we should update the transform.parent property with the mapped Entity that we fetch from the ParentNetwork. + if (isTransform(message) && isRendererTransport && ParentNetwork.getOrNull(message.entityId)) { // Generate a new transform raw data with the parent property included } And every time we recieve a message from the renderer, we should remove the parent property to keep consistency in all CRDT state clients. - if (isTransform(message) && message.type === CrdtMessageType.PUT_COMPONENT && ParentSync.has(message.entityId)) { + if (isTransform(message) && message.type === CrdtMessageType.PUT_COMPONENT && ParentNetwork.has(message.entityId)) { transform.parent = null // Generate a new transform raw data without the parent property included } With this approach, all the clients will have the same Transform, so we avoid the inconsistency of crdt's state. - And when some user wants to update the transform, it has to modify the ParentSync and will update both values, the parent & the network. + And when some user wants to update the transform, it has to modify the ParentNetwork and will update both values, the parent & the network. diff --git a/packages/@dcl/sdk/src/network/parent.ts b/packages/@dcl/sdk/src/network/parent.ts new file mode 100644 index 000000000..a4944a336 --- /dev/null +++ b/packages/@dcl/sdk/src/network/parent.ts @@ -0,0 +1,37 @@ +import { Entity, NetworkEntity, NetworkParent, Transform } from '@dcl/ecs' + +export function parentEntity(entity: Entity, parent: Entity) { + const network = NetworkEntity.getOrNull(parent) + if (!network) { + throw new Error('Please call syncEntity on the parent before parentEntity fn') + } + + // Create network parent component + NetworkParent.createOrReplace(entity, network) + + // If we dont have a transform for this entity, create an empty one to send it to the renderer + if (!Transform.getOrNull(entity)) { + Transform.create(entity) + } +} +/** + * enum SyncEnum { + * PARENT_DOOR, + * CHILD_DOOR + * } + +* // Create parent + * const parent = engine.addEntity() // 512 + * Transform.create(parent, { position: 4, 1, 4 }) + * syncEntity(parent, [], SyncEnum.PARENT_DOOR) // { networkId: 0, entityId: 1 } + +* // Create Child + * const child = engine.addEntity() // 513 + * Transform.create(child) + * parentEntity(child, parent) // NetworkParent => { networkId: 0, entityId: 1 } + * syncEntity(child, [Transform.componentId], SyncEnum.CHILD_DOOR) // { networkId: 0, entityId: 2 } + * + * // Now we should see the child on the position 4,1,4 on every client. + * // But WHAAAAAAAAAAT if we create a new entity on user click, and change the parenting to that entity with the position of the user. + * // TODO: this case. + */ diff --git a/packages/@dcl/sdk/src/network/sync-entity.ts b/packages/@dcl/sdk/src/network/sync-entity.ts index df2ec9c47..50ef007e0 100644 --- a/packages/@dcl/sdk/src/network/sync-entity.ts +++ b/packages/@dcl/sdk/src/network/sync-entity.ts @@ -1,14 +1,21 @@ import { Entity, NetworkEntity, SyncComponents } from '@dcl/ecs' import { myProfile } from './utils' -export function syncEntity(entity: Entity, componentIds: number[], id?: number) { +export function syncEntity(entityId: Entity, componentIds: number[], entityEnumId?: number) { + // Profile not initialized if (!myProfile?.networkId) { - throw new Error('USER_ID NOT INITIALIZED') + throw new Error('Profile not initialized. Called syncEntity inside the main() function.') } - // It it has an custom Id, then the networkId and the entityId should be the same for everyone - // If not, use the profile as the networkId, and the real entityId to then map the entity + + // If there is an entityEnumId, it means is the same entity for all the clients created on the main funciton. + // So the networkId should be the same in all the clients to avoid re-creating this entity. + // For this case we use networkId = 0. + // If is not defined, then is a entity created in runtime (what we called dynamic/runtime entities). + // We use the networkId generated by the user address to identify this entity through the network const networkEntity = - id !== undefined ? { entityId: id as Entity, networkId: 0 } : { entityId: entity, networkId: myProfile.networkId } - NetworkEntity.createOrReplace(entity, networkEntity) - SyncComponents.createOrReplace(entity, { componentIds }) + entityEnumId !== undefined + ? { entityId: entityEnumId as Entity, networkId: 0 } + : { entityId, networkId: myProfile.networkId } + NetworkEntity.createOrReplace(entityId, networkEntity) + SyncComponents.createOrReplace(entityId, { componentIds }) }