Skip to content

Commit

Permalink
wip sync parent entities
Browse files Browse the repository at this point in the history
  • Loading branch information
gonpombo8 committed Nov 21, 2023
1 parent 257fb1c commit 336e422
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 40 deletions.
13 changes: 11 additions & 2 deletions packages/@dcl/ecs/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -64,4 +65,12 @@ export const SyncComponents: (
/* @__PURE__ */
export const NetworkEntity: (
engine: Pick<IEngine, 'defineComponent'>
) => LastWriteWinElementSetComponentDefinition<INetowrkEntityType> = (engine) => defineEntityNetwork(engine)
) => LastWriteWinElementSetComponentDefinition<INetowrkEntityType> = (engine) => defineNetworkEntity(engine)

/**
* @alpha
*/
/* @__PURE__ */
export const NetworkParent: (
engine: Pick<IEngine, 'defineComponent'>
) => LastWriteWinElementSetComponentDefinition<INetowrkParentType> = (engine) => defineNetworkParent(engine)
20 changes: 20 additions & 0 deletions packages/@dcl/ecs/src/components/manual/NetworkParent.ts
Original file line number Diff line number Diff line change
@@ -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<INetowrkParentType>

function defineNetworkParentComponent(engine: Pick<IEngine, 'defineComponent'>) {
const EntityNetwork = engine.defineComponent('core-schema::Network-Parent', {
networkId: Schemas.Int64,
entityId: Schemas.Entity
})
return EntityNetwork
}

export default defineNetworkParentComponent
1 change: 1 addition & 0 deletions packages/@dcl/ecs/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
8 changes: 7 additions & 1 deletion packages/@dcl/ecs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
AnimatorComponentDefinitionExtended,
ISyncComponents,
TweenComponentDefinitionExtended,
INetowrkEntity
INetowrkEntity,
INetowrkParent
} from './components/types'
import { NameComponent } from './components/manual/Name'

Expand All @@ -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'
Expand Down
38 changes: 35 additions & 3 deletions packages/@dcl/ecs/src/serialization/crdt/network/utils.ts
Original file line number Diff line number Diff line change
@@ -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 [
Expand Down Expand Up @@ -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()
}
61 changes: 49 additions & 12 deletions packages/@dcl/ecs/src/systems/crdt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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.
Expand Down Expand Up @@ -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 }
}

Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
29 changes: 14 additions & 15 deletions packages/@dcl/sdk/src/network/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,47 +74,46 @@ 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
const parent = engine.addEntity()
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.
37 changes: 37 additions & 0 deletions packages/@dcl/sdk/src/network/parent.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
21 changes: 14 additions & 7 deletions packages/@dcl/sdk/src/network/sync-entity.ts
Original file line number Diff line number Diff line change
@@ -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 })
}

0 comments on commit 336e422

Please sign in to comment.