-
Notifications
You must be signed in to change notification settings - Fork 95
feat: SDK7 implement ADR-245 (player components) #5724
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
leanmendoza
wants to merge
5
commits into
dev
Choose a base branch
from
feat/implement-sdk7-avatar-components
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
232
browser-interface/packages/shared/apis/host/runtime7/avatar/ecs.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 } | ||
|
|
||
| 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 | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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
Reviewstate 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
Scenefor avatars, and probably Explorer-Alpha goes with that approach, it'd be embedded in the explorer itself.