Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions browser-interface/packages/shared/apis/host/EngineAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,69 @@ import type { EventData, ManyEntityAction } from 'shared/protocol/decentraland/k
import { EngineApiServiceDefinition } from 'shared/protocol/decentraland/kernel/apis/engine_api.gen'
import type { PortContext } from './context'

import { avatarSdk7Ecs, avatarSdk7MessageObservable } from './runtime7/avatar'
import { DeleteComponent } from './runtime7/serialization/crdt/deleteComponent'
import { ReadWriteByteBuffer } from './runtime7/serialization/ByteBuffer'
import { Sdk7ComponentIds } from './runtime7/avatar/ecs'
import { buildAvatarTransformMessage } from './runtime7/serialization/transform'

function getParcelNumber(x: number, z: number) {
return z * 100e8 + x
}

export function registerEngineApiServiceServerImplementation(port: RpcServerPort<PortContext>) {
codegen.registerService(
port,
EngineApiServiceDefinition,
async (): Promise<RpcServerModule<EngineApiServiceDefinition, PortContext>> => {
async (port, ctx): Promise<RpcServerModule<EngineApiServiceDefinition, PortContext>> => {
let sdk7AvatarUpdates: Uint8Array[] = []

if (ctx.sdk7) {
const tempReusableBuffer = new ReadWriteByteBuffer()
const parcels: Set<number> = new Set()
if (!ctx.sceneData.isGlobalScene) {
ctx.sceneData.entity.pointers.forEach((pointer) => {
const [x, z] = pointer.split(',').map((n) => parseInt(n, 10))
parcels.add(getParcelNumber(x, z))
})
}

const [baseX, baseZ] = ctx.sceneData.entity.metadata?.scene?.base?.split(',').map((n) => parseInt(n, 10)) ?? [
0, 0
]
const offset = { x: baseX * 16.0, y: 0, z: baseZ * 16.0 }

sdk7AvatarUpdates = avatarSdk7Ecs.getState()

avatarSdk7MessageObservable.on('BinaryMessage', (message) => {
sdk7AvatarUpdates.push(message)
})

avatarSdk7MessageObservable.on('RemoveAvatar', (message) => {
sdk7AvatarUpdates.push(message.data)
ctx.avatarEntityInsideScene.delete(message.entity)
})

avatarSdk7MessageObservable.on('ChangePosition', (message) => {
const isInsideScene =
ctx.sceneData.isGlobalScene || parcels.has(getParcelNumber(message.parcel.x, message.parcel.z))
const wasInsideScene = ctx.avatarEntityInsideScene.get(message.entity) || false
if (isInsideScene) {
sdk7AvatarUpdates.push(buildAvatarTransformMessage(message.entity, message.ts, message.data, offset))

if (!wasInsideScene) {
ctx.avatarEntityInsideScene.set(message.entity, true)
}
} else if (wasInsideScene) {
ctx.avatarEntityInsideScene.set(message.entity, false)

tempReusableBuffer.resetBuffer()
DeleteComponent.write(message.entity, Sdk7ComponentIds.TRANSFORM, message.ts, tempReusableBuffer)
sdk7AvatarUpdates.push(tempReusableBuffer.toCopiedBinary())
}
})
}

return {
async sendBatch(_req: ManyEntityAction, ctx) {
// TODO: (2023/01/06) `sendBatch` is still used by sdk7 scenes to retreive
Expand Down Expand Up @@ -45,7 +103,10 @@ export function registerEngineApiServiceServerImplementation(port: RpcServerPort
payload: req.data
})

return { data: [ret.payload] }
const avatarStates = sdk7AvatarUpdates
sdk7AvatarUpdates = []

return { data: [ret.payload, ...avatarStates] }
},

// @deprecated
Expand Down
7 changes: 4 additions & 3 deletions browser-interface/packages/shared/apis/host/Testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { RpcServerPort } from '@dcl/rpc/dist/types'
import { TestingServiceDefinition } from 'shared/protocol/decentraland/kernel/apis/testing.gen'
import type { PortContextService } from './context'

declare var __DCL_TESTING_EXTENSION__: any
declare let __DCL_TESTING_EXTENSION__: any

export function registerTestingServiceServerImplementation(port: RpcServerPort<PortContextService<'logger'>>) {
codegen.registerService(port, TestingServiceDefinition, async () => ({
Expand All @@ -16,8 +16,9 @@ export function registerTestingServiceServerImplementation(port: RpcServerPort<P
return {}
},
async setCameraTransform(transform) {
if (typeof __DCL_TESTING_EXTENSION__ !== 'undefined') return __DCL_TESTING_EXTENSION__.setCameraTransform(transform)
if (typeof __DCL_TESTING_EXTENSION__ !== 'undefined')
return __DCL_TESTING_EXTENSION__.setCameraTransform(transform)
return {}
}
}))
}
}
11 changes: 7 additions & 4 deletions browser-interface/packages/shared/apis/host/context.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { RpcClientPort } from '@dcl/rpc'
import type { RpcClientModule } from '@dcl/rpc/dist/codegen'
import type { ILogger } from 'lib/logger'
import type { LoadableScene } from 'shared/types'
import type { PermissionItem } from 'shared/protocol/decentraland/kernel/apis/permissions.gen'
import type { EventData } from 'shared/protocol/decentraland/kernel/apis/engine_api.gen'
import type { RpcClientPort } from '@dcl/rpc'
import type { PermissionItem } from 'shared/protocol/decentraland/kernel/apis/permissions.gen'
import type { RpcSceneControllerServiceDefinition } from 'shared/protocol/decentraland/renderer/renderer_services/scene_controller.gen'
import type { RpcClientModule } from '@dcl/rpc/dist/codegen'
import { EntityAction } from 'shared/protocol/decentraland/sdk/ecs6/engine_interface_ecs6.gen'
import type { LoadableScene } from 'shared/types'
import type { Entity } from './runtime7/engine/entity'

type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }

Expand All @@ -20,6 +21,8 @@ export type PortContext = {
subscribedEvents: Set<string>
events: EventData[]

avatarEntityInsideScene: Map<Entity, boolean>

// @deprecated
sendBatch(actions: EntityAction[]): void
sendSceneEvent<K extends keyof IEvents>(id: K, event: IEvents[K]): void
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This code is extracted from `js-sdk-toolchain` where is holded the SDK7 codebase. Importing the all library just for some of behavior doesn't seems to be
232 changes: 232 additions & 0 deletions browser-interface/packages/shared/apis/host/runtime7/avatar/ecs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { NewProfileForRenderer } from 'lib/decentraland/profiles/transformations'
import { ReceiveUserExpressionMessage } from 'shared/comms/interface/types'
import { PBAvatarBase } from '../../../../protocol/decentraland/sdk/components/avatar_base.gen'
import { PBAvatarEmoteCommand } from '../../../../protocol/decentraland/sdk/components/avatar_emote_command.gen'
import { PBAvatarEquippedData } from '../../../../protocol/decentraland/sdk/components/avatar_equipped_data.gen'
import { PBPlayerIdentityData } from '../../../../protocol/decentraland/sdk/components/player_identity_data.gen'
import { Entity } from '../engine/entity'
import { ReadWriteByteBuffer } from '../serialization/ByteBuffer'
import { AppendValueOperation } from '../serialization/crdt/appendValue'
import { DeleteEntity } from '../serialization/crdt/deleteEntity'
import { PutComponentOperation } from '../serialization/crdt/putComponent'

const MAX_ENTITY_VERSION = 0xffff
const AVATAR_RESERVED_ENTITY_NUMBER = { from: 10, to: 200 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is kind of a limitation right ? We can't support more that 190 users 🤔 ?
What when we have the sdk7 avatar scene and foreing entities ? How this two things are going to live together ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the range defined in the ADR is from 32 to 256 (224 players limit). AFAIK, the current limit in the Foundation client is 100 players. I'm not sure how realistic it is to think about more than 224 players but the ADR-245 is in Review state and comments and modifications are welcome.
Regarding the foreign entities, I'm not sure about how they and this are going to live together as I don't know what is the ETA to get the foreign entities a fact. The first (and original) idea was to do this with the Foreign entities (in the Protocol Squad), but due to the complexity and the scope of this, we decided to move with this approach.

Bevy and Godot implementation doesn't have a JavaScript Scene for avatars, and probably Explorer-Alpha goes with that approach, it'd be embedded in the explorer itself.


export const Sdk7ComponentIds = {
TRANSFORM: 1,
AVATAR_BASE: 1087,
AVATAR_EMOTE_COMMAND: 1088,
PLAYER_IDENTITY_DATA: 1089,
AVATAR_EQUIPPED_DATA: 1091
}

export function createTinyEcs() {
const avatarEntity = new Map<string, Entity>()
const entities: Map<number, { version: number; live: boolean }> = new Map()
const componentsTimestamp: Map<number, Map<number, { lastMessageData?: Uint8Array; ts: number }>> = new Map()
const crdtReusableBuffer = new ReadWriteByteBuffer()
const transformReusableBuffer = new ReadWriteByteBuffer()

function createNewEntity(): Entity {
for (
let entityNumber = AVATAR_RESERVED_ENTITY_NUMBER.from;
entityNumber < AVATAR_RESERVED_ENTITY_NUMBER.to;
entityNumber++
) {
const currentEntity = entities.get(entityNumber)
if (!currentEntity) {
entities.set(entityNumber, { version: 0, live: true })
return entityNumber as Entity
} else if (!currentEntity.live && currentEntity.version < MAX_ENTITY_VERSION) {
currentEntity.live = true
currentEntity.version++
return (((entityNumber & MAX_ENTITY_VERSION) | ((currentEntity.version & MAX_ENTITY_VERSION) << 16)) >>>
0) as Entity
}
}

throw new Error("Can't create more entities")
}

function ensureAvatarEntityId(userId: string) {
const entity = avatarEntity.get(userId)
if (entity) {
return entity
}

const newEntity = createNewEntity()
avatarEntity.set(userId, newEntity)
return newEntity
}

function removeAvatarEntityId(userId: string): Uint8Array {
const entity = avatarEntity.get(userId)
if (entity) {
const entityNumber = entity & MAX_ENTITY_VERSION
const entityVersion = ((entity & 0xffff0000) >> 16) & MAX_ENTITY_VERSION

if (entities.get(entityNumber)?.version === entityVersion) {
entities.set(entityNumber, { version: entityVersion, live: false })
}

avatarEntity.delete(userId)
for (const [_componentId, data] of componentsTimestamp) {
data.delete(entity)
}

transformReusableBuffer.resetBuffer()
DeleteEntity.write(entity, transformReusableBuffer)
return transformReusableBuffer.toCopiedBinary()
}
return new Uint8Array()
}

function getComponentTimestamp(componentId: number) {
const component = componentsTimestamp.get(componentId)
if (component) {
return component
}

componentsTimestamp.set(componentId, new Map())
return componentsTimestamp.get(componentId)!
}

function appendAvatarEmoteCommand(entity: Entity, data: ReceiveUserExpressionMessage): Uint8Array {
const avatarEmoteCommandComponent = getComponentTimestamp(Sdk7ComponentIds.AVATAR_EMOTE_COMMAND)

const writer = PBAvatarEmoteCommand.encode({
emoteCommand: {
emoteUrn: data.expressionId,
loop: false // TODO: how to know if is loopable
}
})

const buffer = new Uint8Array(writer.finish(), 0, writer.len)

const timestamp = (avatarEmoteCommandComponent.get(entity)?.ts || 0) + 1
AppendValueOperation.write(entity, timestamp, Sdk7ComponentIds.AVATAR_EMOTE_COMMAND, buffer, crdtReusableBuffer)

avatarEmoteCommandComponent.set(entity, { ts: timestamp })

return crdtReusableBuffer.toCopiedBinary()
}

function updateProfile(entity: Entity, data: NewProfileForRenderer): Uint8Array[] {
const msgs: Uint8Array[] = []
const playerIdentityComponent = getComponentTimestamp(Sdk7ComponentIds.PLAYER_IDENTITY_DATA)
const avatarBaseComponent = getComponentTimestamp(Sdk7ComponentIds.AVATAR_BASE)
const avatarEquippedComponent = getComponentTimestamp(Sdk7ComponentIds.AVATAR_EQUIPPED_DATA)

// Player identity is sent only once
if (playerIdentityComponent.get(entity) === undefined) {
crdtReusableBuffer.resetBuffer()

const writer = PBPlayerIdentityData.encode({
address: data.userId,
isGuest: data.hasConnectedWeb3
})
const buffer = new Uint8Array(writer.finish(), 0, writer.len)
const timestamp = (playerIdentityComponent.get(entity)?.ts || 0) + 1
PutComponentOperation.write(entity, timestamp, Sdk7ComponentIds.PLAYER_IDENTITY_DATA, buffer, crdtReusableBuffer)

const messageData = crdtReusableBuffer.toCopiedBinary()
playerIdentityComponent.set(entity, { ts: timestamp, lastMessageData: messageData })
msgs.push(messageData)
}

// Update avatar base
{
crdtReusableBuffer.resetBuffer()

const writer = PBAvatarBase.encode({
skinColor: data.avatar.skinColor,
eyesColor: data.avatar.eyeColor,
hairColor: data.avatar.hairColor,
bodyShapeUrn: data.avatar.bodyShape,
name: data.name
})
const buffer = new Uint8Array(writer.finish(), 0, writer.len)
const timestamp = (avatarBaseComponent.get(entity)?.ts || 0) + 1
PutComponentOperation.write(entity, timestamp, Sdk7ComponentIds.AVATAR_BASE, buffer, crdtReusableBuffer)

const messageData = crdtReusableBuffer.toCopiedBinary()
avatarBaseComponent.set(entity, { ts: timestamp, lastMessageData: messageData })
msgs.push(messageData)
}

// Update avatar equipped data
{
crdtReusableBuffer.resetBuffer()

const writer = PBAvatarEquippedData.encode({
wearableUrns: data.avatar.wearables,
emotesUrns: (data.avatar.emotes || []).map(($) => $.urn)
})
const buffer = new Uint8Array(writer.finish(), 0, writer.len)
const timestamp = (avatarEquippedComponent.get(entity)?.ts || 0) + 1
PutComponentOperation.write(entity, timestamp, Sdk7ComponentIds.AVATAR_EQUIPPED_DATA, buffer, crdtReusableBuffer)

const messageData = crdtReusableBuffer.toCopiedBinary()
avatarEquippedComponent.set(entity, { ts: timestamp, lastMessageData: messageData })
msgs.push(messageData)
}
return msgs
}

function computeNextAvatarTransformTimestamp(entity: Entity) {
const transformComponent = getComponentTimestamp(Sdk7ComponentIds.TRANSFORM)
const timestamp = (transformComponent.get(entity)?.ts || 0) + 1
transformComponent.set(entity, { ts: timestamp })
return timestamp
}

function getState(): Uint8Array[] {
const playerIdentityComponent = getComponentTimestamp(Sdk7ComponentIds.PLAYER_IDENTITY_DATA)
const avatarBaseComponent = getComponentTimestamp(Sdk7ComponentIds.AVATAR_BASE)
const avatarEquippedComponent = getComponentTimestamp(Sdk7ComponentIds.AVATAR_EQUIPPED_DATA)

const msgs: Uint8Array[] = []
for (
let entityNumber = AVATAR_RESERVED_ENTITY_NUMBER.from;
entityNumber < AVATAR_RESERVED_ENTITY_NUMBER.to;
entityNumber++
) {
const currentEntity = entities.get(entityNumber)
if (currentEntity && currentEntity.live) {
const entityId = (((entityNumber & MAX_ENTITY_VERSION) |
((currentEntity.version & MAX_ENTITY_VERSION) << 16)) >>>
0) as Entity

const playerIdentityData = playerIdentityComponent.get(entityId)
const avatarBaseData = avatarBaseComponent.get(entityId)
const avatarEquippedData = avatarEquippedComponent.get(entityId)

if (playerIdentityData?.lastMessageData) {
msgs.push(playerIdentityData.lastMessageData)
}

if (avatarBaseData?.lastMessageData) {
msgs.push(avatarBaseData.lastMessageData)
}

if (avatarEquippedData?.lastMessageData) {
msgs.push(avatarEquippedData.lastMessageData)
}
}
}

return msgs
}

return {
ensureAvatarEntityId,
removeAvatarEntityId,

computeNextAvatarTransformTimestamp,
updateProfile,
appendAvatarEmoteCommand,

getState
}
}
Loading