From a13a9487b170af5de1aa09a3a0be969c6ec62432 Mon Sep 17 00:00:00 2001 From: Lean Mendoza Date: Wed, 20 Sep 2023 12:07:40 -0300 Subject: [PATCH 1/5] setup sdk7 protocol functions --- .../packages/shared/apis/host/sdk7/README.md | 1 + .../shared/apis/host/sdk7/engine/entity.ts | 2 + .../sdk7/serialization/ByteBuffer/index.ts | 370 ++++++++++++++++++ .../sdk7/serialization/crdt/appendValue.ts | 57 +++ .../serialization/crdt/crdtMessageProtocol.ts | 73 ++++ .../serialization/crdt/deleteComponent.ts | 51 +++ .../sdk7/serialization/crdt/deleteEntity.ts | 36 ++ .../host/sdk7/serialization/crdt/index.ts | 6 + .../host/sdk7/serialization/crdt/message.ts | 24 ++ .../sdk7/serialization/crdt/putComponent.ts | 55 +++ .../host/sdk7/serialization/crdt/types.ts | 187 +++++++++ 11 files changed, 862 insertions(+) create mode 100644 browser-interface/packages/shared/apis/host/sdk7/README.md create mode 100644 browser-interface/packages/shared/apis/host/sdk7/engine/entity.ts create mode 100644 browser-interface/packages/shared/apis/host/sdk7/serialization/ByteBuffer/index.ts create mode 100644 browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/appendValue.ts create mode 100644 browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/crdtMessageProtocol.ts create mode 100644 browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteComponent.ts create mode 100644 browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteEntity.ts create mode 100644 browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/index.ts create mode 100644 browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/message.ts create mode 100644 browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/putComponent.ts create mode 100644 browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/types.ts diff --git a/browser-interface/packages/shared/apis/host/sdk7/README.md b/browser-interface/packages/shared/apis/host/sdk7/README.md new file mode 100644 index 0000000000..a3fcbc2c00 --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/README.md @@ -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 \ No newline at end of file diff --git a/browser-interface/packages/shared/apis/host/sdk7/engine/entity.ts b/browser-interface/packages/shared/apis/host/sdk7/engine/entity.ts new file mode 100644 index 0000000000..e7f016ffdf --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/engine/entity.ts @@ -0,0 +1,2 @@ +export type Entity = number +export type uint32 = number diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/ByteBuffer/index.ts b/browser-interface/packages/shared/apis/host/sdk7/serialization/ByteBuffer/index.ts new file mode 100644 index 0000000000..e317de669d --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/serialization/ByteBuffer/index.ts @@ -0,0 +1,370 @@ +import * as utf8 from '@protobufjs/utf8' + +/** + * Take the max between currentSize and intendedSize and then plus 1024. Then, + * find the next nearer multiple of 1024. + * @param currentSize - number + * @param intendedSize - number + * @returns the calculated number + */ +function getNextSize(currentSize: number, intendedSize: number) { + const minNewSize = Math.max(currentSize, intendedSize) + 1024 + return Math.ceil(minNewSize / 1024) * 1024 +} + +const defaultInitialCapacity = 10240 + +/** + * ByteBuffer is a wrapper of DataView which also adds a read and write offset. + * Also in a write operation it resizes the buffer is being used if it needs. + * + * - Use read and write function to generate or consume data. + * - Use set and get only if you are sure that you're doing. + * + * It always passes littleEndian param as true + */ +export class ReadWriteByteBuffer implements ByteBuffer { + _buffer: Uint8Array + view: DataView + woffset: number + roffset: number + /** + * @param buffer - The initial buffer, provide a buffer if you need to set "initial capacity" + * @param readingOffset - Set the cursor where begins to read. Default 0 + * @param writingOffset - Set the cursor to not start writing from the begin of it. Defaults to the buffer size + */ + constructor(buffer?: Uint8Array | undefined, readingOffset?: number | undefined, writingOffset?: number | undefined) { + this._buffer = buffer || new Uint8Array(defaultInitialCapacity) + this.view = new DataView(this._buffer.buffer, this._buffer.byteOffset) + this.woffset = writingOffset ?? (buffer ? this._buffer.length : null) ?? 0 + this.roffset = readingOffset ?? 0 + } + + /** + * Increement the write offset and resize the buffer if it needs. + */ + #woAdd(amount: number) { + if (this.woffset + amount > this._buffer.byteLength) { + const newsize = getNextSize(this._buffer.byteLength, this.woffset + amount) + const newBuffer = new Uint8Array(newsize) + newBuffer.set(this._buffer) + const oldOffset = this._buffer.byteOffset + this._buffer = newBuffer + this.view = new DataView(this._buffer.buffer, oldOffset) + } + + this.woffset += amount + return this.woffset - amount + } + + /** + * Increment the read offset and throw an error if it's trying to read + * outside the bounds. + */ + #roAdd(amount: number) { + if (this.roffset + amount > this.woffset) { + throw new Error('Outside of the bounds of writen data.') + } + + this.roffset += amount + return this.roffset - amount + } + + buffer(): Uint8Array { + return this._buffer + } + bufferLength(): number { + return this._buffer.length + } + resetBuffer(): void { + this.roffset = 0 + this.woffset = 0 + } + currentReadOffset(): number { + return this.roffset + } + currentWriteOffset(): number { + return this.woffset + } + incrementReadOffset(amount: number): number { + return this.#roAdd(amount) + } + remainingBytes(): number { + return this.woffset - this.roffset + } + readFloat32(): number { + return this.view.getFloat32(this.#roAdd(4), true) // littleEndian = true + } + readFloat64(): number { + return this.view.getFloat64(this.#roAdd(8), true) // littleEndian = true + } + readInt8(): number { + return this.view.getInt8(this.#roAdd(1)) + } + readInt16(): number { + return this.view.getInt16(this.#roAdd(2), true) // littleEndian = true + } + readInt32(): number { + return this.view.getInt32(this.#roAdd(4), true) // littleEndian = true + } + readInt64(): bigint { + return this.view.getBigInt64(this.#roAdd(8), true) // littleEndian = true + } + readUint8(): number { + return this.view.getUint8(this.#roAdd(1)) + } + readUint16(): number { + return this.view.getUint16(this.#roAdd(2), true) // littleEndian = true + } + readUint32(): number { + return this.view.getUint32(this.#roAdd(4), true) // littleEndian = true + } + readUint64(): bigint { + return this.view.getBigUint64(this.#roAdd(8), true) // littleEndian = true + } + readBuffer() { + const length = this.view.getUint32(this.#roAdd(4), true) // littleEndian = true + return this._buffer.subarray(this.#roAdd(length), this.#roAdd(0)) + } + readUtf8String() { + const length = this.view.getUint32(this.#roAdd(4), true) // littleEndian = true + return utf8.read(this._buffer, this.#roAdd(length), this.#roAdd(0)) + } + incrementWriteOffset(amount: number): number { + return this.#woAdd(amount) + } + toBinary() { + return this._buffer.subarray(0, this.woffset) + } + toCopiedBinary() { + return new Uint8Array(this.toBinary()) + } + writeBuffer(value: Uint8Array, writeLength: boolean = true) { + if (writeLength) { + this.writeUint32(value.byteLength) + } + + const o = this.#woAdd(value.byteLength) + this._buffer.set(value, o) + } + writeUtf8String(value: string, writeLength: boolean = true) { + const byteLength = utf8.length(value) + + if (writeLength) { + this.writeUint32(byteLength) + } + + const o = this.#woAdd(byteLength) + + utf8.write(value, this._buffer, o) + } + writeFloat32(value: number): void { + const o = this.#woAdd(4) + this.view.setFloat32(o, value, true) // littleEndian = true + } + writeFloat64(value: number): void { + const o = this.#woAdd(8) + this.view.setFloat64(o, value, true) // littleEndian = true + } + writeInt8(value: number): void { + const o = this.#woAdd(1) + this.view.setInt8(o, value) + } + writeInt16(value: number): void { + const o = this.#woAdd(2) + this.view.setInt16(o, value, true) // littleEndian = true + } + writeInt32(value: number): void { + const o = this.#woAdd(4) + this.view.setInt32(o, value, true) // littleEndian = true + } + writeInt64(value: bigint): void { + const o = this.#woAdd(8) + this.view.setBigInt64(o, value, true) // littleEndian = true + } + writeUint8(value: number): void { + const o = this.#woAdd(1) + this.view.setUint8(o, value) + } + writeUint16(value: number): void { + const o = this.#woAdd(2) + this.view.setUint16(o, value, true) // littleEndian = true + } + writeUint32(value: number): void { + const o = this.#woAdd(4) + this.view.setUint32(o, value, true) // littleEndian = true + } + writeUint64(value: bigint): void { + const o = this.#woAdd(8) + this.view.setBigUint64(o, value, true) // littleEndian = true + } + // DataView Proxy + getFloat32(offset: number): number { + return this.view.getFloat32(offset, true) // littleEndian = true + } + getFloat64(offset: number): number { + return this.view.getFloat64(offset, true) // littleEndian = true + } + getInt8(offset: number): number { + return this.view.getInt8(offset) + } + getInt16(offset: number): number { + return this.view.getInt16(offset, true) // littleEndian = true + } + getInt32(offset: number): number { + return this.view.getInt32(offset, true) // littleEndian = true + } + getInt64(offset: number): bigint { + return this.view.getBigInt64(offset, true) // littleEndian = true + } + getUint8(offset: number): number { + return this.view.getUint8(offset) + } + getUint16(offset: number): number { + return this.view.getUint16(offset, true) // littleEndian = true + } + getUint32(offset: number): number { + return this.view.getUint32(offset, true) // littleEndian = true >>> 0 + } + getUint64(offset: number): bigint { + return this.view.getBigUint64(offset, true) // littleEndian = true + } + setFloat32(offset: number, value: number): void { + this.view.setFloat32(offset, value, true) // littleEndian = true + } + setFloat64(offset: number, value: number): void { + this.view.setFloat64(offset, value, true) // littleEndian = true + } + setInt8(offset: number, value: number): void { + this.view.setInt8(offset, value) + } + setInt16(offset: number, value: number): void { + this.view.setInt16(offset, value, true) // littleEndian = true + } + setInt32(offset: number, value: number): void { + this.view.setInt32(offset, value, true) // littleEndian = true + } + setInt64(offset: number, value: bigint): void { + this.view.setBigInt64(offset, value, true) // littleEndian = true + } + setUint8(offset: number, value: number): void { + this.view.setUint8(offset, value) + } + setUint16(offset: number, value: number): void { + this.view.setUint16(offset, value, true) // littleEndian = true + } + setUint32(offset: number, value: number): void { + this.view.setUint32(offset, value, true) // littleEndian = true + } + setUint64(offset: number, value: bigint): void { + this.view.setBigUint64(offset, value, true) // littleEndian = true + } +} + +/** + * @public + */ +export interface ByteBuffer { + /** + * @returns The entire current Uint8Array. + * + * WARNING: if the buffer grows, the view had changed itself, + * and the reference will be a invalid one. + */ + buffer(): Uint8Array + /** + * @returns The capacity of the current buffer + */ + bufferLength(): number + /** + * Resets byteBuffer to avoid creating a new one + */ + resetBuffer(): void + /** + * @returns The current read offset + */ + currentReadOffset(): number + /** + * @returns The current write offset + */ + currentWriteOffset(): number + /** + * Reading purpose + * Returns the previuos offsset size before incrementing + */ + incrementReadOffset(amount: number): number + /** + * @returns How many bytes are available to read. + */ + remainingBytes(): number + readFloat32(): number + readFloat64(): number + readInt8(): number + readInt16(): number + readInt32(): number + readInt64(): bigint + readUint8(): number + readUint16(): number + readUint32(): number + readUint64(): bigint + readBuffer(): Uint8Array + readUtf8String(): string + /** + * Writing purpose + */ + /** + * Increment offset + * @param amount - how many bytes + * @returns The offset when this reserving starts. + */ + incrementWriteOffset(amount: number): number + /** + * Take care using this function, if you modify the data after, the + * returned subarray will change too. If you'll modify the content of the + * bytebuffer, maybe you want to use toCopiedBinary() + * + * @returns The subarray from 0 to offset as reference. + */ + toBinary(): Uint8Array + + /** + * Safe copied buffer of the current data of ByteBuffer + * + * @returns The subarray from 0 to offset. + */ + toCopiedBinary(): Uint8Array + + writeUtf8String(value: string, writeLength?: boolean): void + writeBuffer(value: Uint8Array, writeLength?: boolean): void + writeFloat32(value: number): void + writeFloat64(value: number): void + writeInt8(value: number): void + writeInt16(value: number): void + writeInt32(value: number): void + writeInt64(value: bigint): void + writeUint8(value: number): void + writeUint16(value: number): void + writeUint32(value: number): void + writeUint64(value: bigint): void + // Dataview Proxy + getFloat32(offset: number): number + getFloat64(offset: number): number + getInt8(offset: number): number + getInt16(offset: number): number + getInt32(offset: number): number + getInt64(offset: number): bigint + getUint8(offset: number): number + getUint16(offset: number): number + getUint32(offset: number): number + getUint64(offset: number): bigint + setFloat32(offset: number, value: number): void + setFloat64(offset: number, value: number): void + setInt8(offset: number, value: number): void + setInt16(offset: number, value: number): void + setInt32(offset: number, value: number): void + setInt64(offset: number, value: bigint): void + setUint8(offset: number, value: number): void + setUint16(offset: number, value: number): void + setUint32(offset: number, value: number): void + setUint64(offset: number, value: bigint): void +} diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/appendValue.ts b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/appendValue.ts new file mode 100644 index 0000000000..1a1cfe8fac --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/appendValue.ts @@ -0,0 +1,57 @@ +import { CrdtMessageProtocol } from './crdtMessageProtocol' +import { Entity } from '../../engine/entity' +import { ByteBuffer } from '../ByteBuffer' +import { AppendValueMessage, CrdtMessageType, CRDT_MESSAGE_HEADER_LENGTH } from './types' + +/** + * @public + */ +export namespace AppendValueOperation { + export const MESSAGE_HEADER_LENGTH = 16 + + /** + * Call this function for an optimal writing data passing the ByteBuffer + * already allocated + */ + export function write(entity: Entity, timestamp: number, componentId: number, data: Uint8Array, buf: ByteBuffer) { + // reserve the beginning + const startMessageOffset = buf.incrementWriteOffset(CRDT_MESSAGE_HEADER_LENGTH + MESSAGE_HEADER_LENGTH) + + // write body + buf.writeBuffer(data, false) + const messageLength = buf.currentWriteOffset() - startMessageOffset + + // Write CrdtMessage header + buf.setUint32(startMessageOffset, messageLength) + buf.setUint32(startMessageOffset + 4, CrdtMessageType.APPEND_VALUE) + + // Write ComponentOperation header + buf.setUint32(startMessageOffset + 8, entity as number) + buf.setUint32(startMessageOffset + 12, componentId) + buf.setUint32(startMessageOffset + 16, timestamp) + const newLocal = messageLength - MESSAGE_HEADER_LENGTH - CRDT_MESSAGE_HEADER_LENGTH + buf.setUint32(startMessageOffset + 20, newLocal) + } + + export function read(buf: ByteBuffer): AppendValueMessage | null { + const header = CrdtMessageProtocol.readHeader(buf) + + /* istanbul ignore if */ + if (!header) { + return null + } + + /* istanbul ignore if */ + if (header.type !== CrdtMessageType.APPEND_VALUE) { + throw new Error('AppendValueOperation tried to read another message type.') + } + + return { + ...header, + entityId: buf.readUint32() as Entity, + componentId: buf.readUint32(), + timestamp: buf.readUint32(), + data: buf.readBuffer() + } + } +} diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/crdtMessageProtocol.ts b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/crdtMessageProtocol.ts new file mode 100644 index 0000000000..5d4b6fbe56 --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/crdtMessageProtocol.ts @@ -0,0 +1,73 @@ +import { ByteBuffer } from '../ByteBuffer' +import { CrdtMessageType, CrdtMessageHeader, CRDT_MESSAGE_HEADER_LENGTH } from './types' + +/** + * @public + */ +export namespace CrdtMessageProtocol { + /** + * Validate if the message incoming is completed + * @param buf - ByteBuffer + */ + export function validate(buf: ByteBuffer) { + const rem = buf.remainingBytes() + if (rem < CRDT_MESSAGE_HEADER_LENGTH) { + return false + } + + const messageLength = buf.getUint32(buf.currentReadOffset()) + if (rem < messageLength) { + return false + } + + return true + } + + /** + * Get the current header, consuming the bytes involved. + * @param buf - ByteBuffer + * @returns header or null if there is no validated message + */ + export function readHeader(buf: ByteBuffer): CrdtMessageHeader | null { + if (!validate(buf)) { + return null + } + + return { + length: buf.readUint32(), + type: buf.readUint32() as CrdtMessageType + } + } + + /** + * Get the current header, without consuming the bytes involved. + * @param buf - ByteBuffer + * @returns header or null if there is no validated message + */ + export function getHeader(buf: ByteBuffer): CrdtMessageHeader | null { + if (!validate(buf)) { + return null + } + + const currentOffset = buf.currentReadOffset() + return { + length: buf.getUint32(currentOffset), + type: buf.getUint32(currentOffset + 4) as CrdtMessageType + } + } + + /** + * Consume the incoming message without processing it. + * @param buf - ByteBuffer + * @returns true in case of success or false if there is no valid message. + */ + export function consumeMessage(buf: ByteBuffer): boolean { + const header = getHeader(buf) + if (!header) { + return false + } + + buf.incrementReadOffset(header.length) + return true + } +} diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteComponent.ts b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteComponent.ts new file mode 100644 index 0000000000..884ad73ea7 --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteComponent.ts @@ -0,0 +1,51 @@ +import { CrdtMessageProtocol } from './crdtMessageProtocol' +import { Entity } from '../../engine/entity' +import { ByteBuffer } from '../ByteBuffer' +import { CrdtMessageType, CRDT_MESSAGE_HEADER_LENGTH, DeleteComponentMessage } from './types' + +/** + * @public + */ +export namespace DeleteComponent { + export const MESSAGE_HEADER_LENGTH = 12 + + /** + * Write DeleteComponent message + */ + export function write(entity: Entity, componentId: number, timestamp: number, buf: ByteBuffer) { + // reserve the beginning + const messageLength = CRDT_MESSAGE_HEADER_LENGTH + MESSAGE_HEADER_LENGTH + const startMessageOffset = buf.incrementWriteOffset(messageLength) + + // Write CrdtMessage header + buf.setUint32(startMessageOffset, messageLength) + buf.setUint32(startMessageOffset + 4, CrdtMessageType.DELETE_COMPONENT) + + // Write ComponentOperation header + buf.setUint32(startMessageOffset + 8, entity as number) + buf.setUint32(startMessageOffset + 12, componentId) + + buf.setUint32(startMessageOffset + 16, timestamp) + } + + export function read(buf: ByteBuffer): DeleteComponentMessage | null { + const header = CrdtMessageProtocol.readHeader(buf) + + if (!header) { + return null + } + + if (header.type !== CrdtMessageType.DELETE_COMPONENT) { + throw new Error('DeleteComponentOperation tried to read another message type.') + } + + const msg = { + ...header, + entityId: buf.readUint32() as Entity, + componentId: buf.readUint32(), + timestamp: buf.readUint32() + } + + return msg + } +} diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteEntity.ts b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteEntity.ts new file mode 100644 index 0000000000..6ffc12bd60 --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteEntity.ts @@ -0,0 +1,36 @@ +import { CrdtMessageProtocol } from './crdtMessageProtocol' +import { Entity } from '../../engine/entity' +import { ByteBuffer } from '../ByteBuffer' +import { CrdtMessageType, CRDT_MESSAGE_HEADER_LENGTH, DeleteEntityMessage } from './types' + +/** + * @public + */ +export namespace DeleteEntity { + export const MESSAGE_HEADER_LENGTH = 4 + + export function write(entity: Entity, buf: ByteBuffer) { + // Write CrdtMessage header + buf.writeUint32(CRDT_MESSAGE_HEADER_LENGTH + 4) + buf.writeUint32(CrdtMessageType.DELETE_ENTITY) + + // body + buf.writeUint32(entity) + } + + export function read(buf: ByteBuffer): DeleteEntityMessage | null { + const header = CrdtMessageProtocol.readHeader(buf) + if (!header) { + return null + } + + if (header.type !== CrdtMessageType.DELETE_ENTITY) { + throw new Error('DeleteEntity tried to read another message type.') + } + + return { + ...header, + entityId: buf.readUint32() as Entity + } + } +} diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/index.ts b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/index.ts new file mode 100644 index 0000000000..9ad24c581a --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/index.ts @@ -0,0 +1,6 @@ +export * from './deleteComponent' +export * from './appendValue' +export * from './deleteEntity' +export * from './putComponent' +export * from './types' +export * from './crdtMessageProtocol' diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/message.ts b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/message.ts new file mode 100644 index 0000000000..3bfef6e317 --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/message.ts @@ -0,0 +1,24 @@ +import { CrdtMessageProtocol } from './crdtMessageProtocol' +import { ByteBuffer } from '../ByteBuffer' +import { CrdtMessageType, CrdtMessage } from './types' +import { PutComponentOperation } from './putComponent' +import { DeleteComponent } from './deleteComponent' +import { DeleteEntity } from './deleteEntity' +import { AppendValueOperation } from './appendValue' + +export function readMessage(buf: ByteBuffer): CrdtMessage | null { + const header = CrdtMessageProtocol.getHeader(buf) + if (!header) return null + + if (header.type === CrdtMessageType.PUT_COMPONENT) { + return PutComponentOperation.read(buf) + } else if (header.type === CrdtMessageType.DELETE_COMPONENT) { + return DeleteComponent.read(buf) + } else if (header.type === CrdtMessageType.APPEND_VALUE) { + return AppendValueOperation.read(buf) + } else if (header.type === CrdtMessageType.DELETE_ENTITY) { + return DeleteEntity.read(buf) + } + + return null +} diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/putComponent.ts b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/putComponent.ts new file mode 100644 index 0000000000..4020d0db6d --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/putComponent.ts @@ -0,0 +1,55 @@ +import { CrdtMessageProtocol } from './crdtMessageProtocol' +import { Entity } from '../../engine/entity' +import { ByteBuffer } from '../ByteBuffer' +import { CrdtMessageType, CRDT_MESSAGE_HEADER_LENGTH, PutComponentMessage } from './types' + +/** + * @public + */ +export namespace PutComponentOperation { + export const MESSAGE_HEADER_LENGTH = 16 + + /** + * Call this function for an optimal writing data passing the ByteBuffer + * already allocated + */ + export function write(entity: Entity, timestamp: number, componentId: number, data: Uint8Array, buf: ByteBuffer) { + // reserve the beginning + const startMessageOffset = buf.incrementWriteOffset(CRDT_MESSAGE_HEADER_LENGTH + MESSAGE_HEADER_LENGTH) + + // write body + buf.writeBuffer(data, false) + const messageLength = buf.currentWriteOffset() - startMessageOffset + + // Write CrdtMessage header + buf.setUint32(startMessageOffset, messageLength) + buf.setUint32(startMessageOffset + 4, CrdtMessageType.PUT_COMPONENT) + + // Write ComponentOperation header + buf.setUint32(startMessageOffset + 8, entity as number) + buf.setUint32(startMessageOffset + 12, componentId) + buf.setUint32(startMessageOffset + 16, timestamp) + const newLocal = messageLength - MESSAGE_HEADER_LENGTH - CRDT_MESSAGE_HEADER_LENGTH + buf.setUint32(startMessageOffset + 20, newLocal) + } + + export function read(buf: ByteBuffer): PutComponentMessage | null { + const header = CrdtMessageProtocol.readHeader(buf) + + if (!header) { + return null + } + + if (header.type !== CrdtMessageType.PUT_COMPONENT) { + throw new Error('PutComponentOperation tried to read another message type.') + } + + return { + ...header, + entityId: buf.readUint32() as Entity, + componentId: buf.readUint32(), + timestamp: buf.readUint32(), + data: buf.readBuffer() + } + } +} diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/types.ts b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/types.ts new file mode 100644 index 0000000000..74aa6b5278 --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/types.ts @@ -0,0 +1,187 @@ +import { Entity, uint32 } from '../../engine/entity' + +/** + * @public + */ +export enum CrdtMessageType { + RESERVED = 0, + + // Component Operation + PUT_COMPONENT = 1, + DELETE_COMPONENT = 2, + + DELETE_ENTITY = 3, + APPEND_VALUE = 4, + + MAX_MESSAGE_TYPE +} + +/** + * Min length = 8 bytes + * All message length including + * @param length - uint32 the length of all message (including the header) + * @param type - define the function which handles the data + * @public + */ +export type CrdtMessageHeader = { + length: uint32 + type: uint32 +} + +/** + * @public + */ +export const CRDT_MESSAGE_HEADER_LENGTH = 8 + +/** + * Min. length = header (8 bytes) + 16 bytes = 24 bytes + * + * @param entity - Uint32 number of the entity + * @param componentId - Uint32 number of id + * @param timestamp - Uint32 Lamport timestamp + * @param data - Uint8[] data of component => length(4 bytes) + block of bytes[0..length-1] + * @public + */ +export type PutComponentMessageBody = { + type: CrdtMessageType.PUT_COMPONENT + entityId: Entity + componentId: number + timestamp: number + data: Uint8Array +} + +/** + * Min. length = header (8 bytes) + 16 bytes = 24 bytes + * + * @param entity - Uint32 number of the entity + * @param componentId - Uint32 number of id + * @param timestamp - Uint32 timestamp + * @param data - Uint8[] data of component => length(4 bytes) + block of bytes[0..length-1] + * @public + */ +export type AppendValueMessageBody = { + type: CrdtMessageType.APPEND_VALUE + entityId: Entity + componentId: number + timestamp: number + data: Uint8Array +} + +/** + * @param entity - Uint32 number of the entity + * @param componentId - Uint32 number of id + * @param timestamp - Uint32 Lamport timestamp + * @public + */ +export type DeleteComponentMessageBody = { + type: CrdtMessageType.DELETE_COMPONENT + entityId: Entity + componentId: number + timestamp: number +} + +/** + * @param entity - uint32 number of the entity + * @public + */ +export type DeleteEntityMessageBody = { + type: CrdtMessageType.DELETE_ENTITY + entityId: Entity +} + +/** + * @public + */ +export type AppendValueMessage = CrdtMessageHeader & AppendValueMessageBody +/** + * @public + */ +export type PutComponentMessage = CrdtMessageHeader & PutComponentMessageBody +/** + * @public + */ +export type DeleteComponentMessage = CrdtMessageHeader & DeleteComponentMessageBody +/** + * @public + */ +export type DeleteEntityMessage = CrdtMessageHeader & DeleteEntityMessageBody + +/** + * @public + */ +export type CrdtMessage = PutComponentMessage | DeleteComponentMessage | DeleteEntityMessage | AppendValueMessage + +/** + * @public + */ +export type CrdtMessageBody = + | PutComponentMessageBody + | DeleteComponentMessageBody + | DeleteEntityMessageBody + | AppendValueMessageBody + +export enum ProcessMessageResultType { + /** + * Typical message and new state set. + * @state CHANGE + * @reason Incoming message has a timestamp greater + */ + StateUpdatedTimestamp = 1, + + /** + * Typical message when it is considered old. + * @state it does NOT CHANGE. + * @reason incoming message has a timestamp lower. + */ + StateOutdatedTimestamp = 2, + + /** + * Weird message, same timestamp and data. + * @state it does NOT CHANGE. + * @reason consistent state between peers. + */ + NoChanges = 3, + + /** + * Less but typical message, same timestamp, resolution by data. + * @state it does NOT CHANGE. + * @reason incoming message has a LOWER data. + */ + StateOutdatedData = 4, + + /** + * Less but typical message, same timestamp, resolution by data. + * @state CHANGE. + * @reason incoming message has a GREATER data. + */ + StateUpdatedData = 5, + + /** + * Entity was previously deleted. + * @state it does NOT CHANGE. + * @reason The message is considered old. + */ + EntityWasDeleted = 6, + + /** + * Entity should be deleted. + * @state CHANGE. + * @reason the state is storing old entities + */ + EntityDeleted = 7 +} + +// we receive LWW, v=6, we have v=5 => we receive with delay the deleteEntity(v=5) +// => we should generate the deleteEntity message effects internally with deleteEntity(v=5), +// but don't resend the deleteEntity +// - (CRDT) addDeletedEntitySet v=5 (with crdt state cleaning) and then LWW v=6 +// - (engine) engine.deleteEntity v=5 + +// we receive LWW, v=7, we have v=5 => we receive with delay the deleteEntity(v=5), deleteEntity(v=6), ..., N +// => we should generate the deleteEntity message effects internally with deleteEntity(v=5), +// but don't resend the deleteEntity +// - (CRDT) addDeletedEntitySet v=5 (with crdt state cleaning) and then LWW v=6 +// - (engine) engine.deleteEntity v=5 + +// msg delete entity: it only should be sent by deleter +// From dbde8753b571bf2507800ebdd2cc2ef4f879f948 Mon Sep 17 00:00:00 2001 From: Lean Mendoza Date: Wed, 20 Sep 2023 15:34:22 -0300 Subject: [PATCH 2/5] wip --- .../packages/shared/apis/host/EngineAPI.ts | 38 +++- .../packages/shared/apis/host/Testing.ts | 7 +- .../packages/shared/apis/host/context.ts | 3 + .../shared/apis/host/sdk7/avatar/ecs.ts | 206 ++++++++++++++++++ .../shared/apis/host/sdk7/avatar/index.ts | 54 +++++ .../packages/shared/world/SceneWorker.ts | 1 + .../test/unit/RestrictedActions.test.tsx | 5 +- 7 files changed, 307 insertions(+), 7 deletions(-) create mode 100644 browser-interface/packages/shared/apis/host/sdk7/avatar/ecs.ts create mode 100644 browser-interface/packages/shared/apis/host/sdk7/avatar/index.ts diff --git a/browser-interface/packages/shared/apis/host/EngineAPI.ts b/browser-interface/packages/shared/apis/host/EngineAPI.ts index 8fb1abe6fe..61b2bed88f 100644 --- a/browser-interface/packages/shared/apis/host/EngineAPI.ts +++ b/browser-interface/packages/shared/apis/host/EngineAPI.ts @@ -5,11 +5,42 @@ 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 { avatarSdk7MessageObservable } from './sdk7/avatar' + export function registerEngineApiServiceServerImplementation(port: RpcServerPort) { codegen.registerService( port, EngineApiServiceDefinition, - async (): Promise> => { + async (port, ctx): Promise> => { + let avatarUpdates: Uint8Array[] = [] + avatarSdk7MessageObservable.on('BinaryMessage', (message) => { + avatarUpdates.push(message) + }) + + avatarSdk7MessageObservable.on('RemoveAvatar', (message) => { + avatarUpdates.push(message.data) + ctx.avatarEntityInsideScene.delete(message.entity) + }) + + avatarSdk7MessageObservable.on('ChangePosition', (message) => { + // TODO: Define how to know if an entity is inside the scene + const isInsideScene = true + + const wasInsideScene = ctx.avatarEntityInsideScene.get(message.entity) || false + + if (isInsideScene) { + avatarUpdates.push(message.data) + + if (!wasInsideScene) { + ctx.avatarEntityInsideScene.set(message.entity, true) + } + } else if (wasInsideScene) { + ctx.avatarEntityInsideScene.set(message.entity, false) + + // TODO: Send delete transform + } + }) + return { async sendBatch(_req: ManyEntityAction, ctx) { // TODO: (2023/01/06) `sendBatch` is still used by sdk7 scenes to retreive @@ -45,7 +76,10 @@ export function registerEngineApiServiceServerImplementation(port: RpcServerPort payload: req.data }) - return { data: [ret.payload] } + const avatarStates = avatarUpdates + avatarUpdates = [] + + return { data: [ret.payload, ...avatarStates] } }, // @deprecated diff --git a/browser-interface/packages/shared/apis/host/Testing.ts b/browser-interface/packages/shared/apis/host/Testing.ts index bc5e2ca631..2dd896c470 100644 --- a/browser-interface/packages/shared/apis/host/Testing.ts +++ b/browser-interface/packages/shared/apis/host/Testing.ts @@ -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>) { codegen.registerService(port, TestingServiceDefinition, async () => ({ @@ -16,8 +16,9 @@ export function registerTestingServiceServerImplementation(port: RpcServerPort

= T & { [P in K]-?: T[P] } @@ -20,6 +21,8 @@ export type PortContext = { subscribedEvents: Set events: EventData[] + avatarEntityInsideScene: Map + // @deprecated sendBatch(actions: EntityAction[]): void sendSceneEvent(id: K, event: IEvents[K]): void diff --git a/browser-interface/packages/shared/apis/host/sdk7/avatar/ecs.ts b/browser-interface/packages/shared/apis/host/sdk7/avatar/ecs.ts new file mode 100644 index 0000000000..66b55edf53 --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/avatar/ecs.ts @@ -0,0 +1,206 @@ +import { Entity } from '../engine/entity' +import { ReadWriteByteBuffer } from '../serialization/ByteBuffer' +import { PutComponentOperation } from '../serialization/crdt/putComponent' +import { AppendValueOperation } from '../serialization/crdt/appendValue' +import { DeleteEntity } from '../serialization/crdt/deleteEntity' +// import * as rfc4 from 'shared/protocol/decentraland/kernel/comms/rfc4/comms.gen' +// import { PBAvatarEmoteCommand } from 'shared/protocol/out-ts/decentraland/sdk/components/avatar_emote_command.gen.ts' + +import * as rfc4 from '../../../../protocol/decentraland/kernel/comms/rfc4/comms.gen' +import { PBAvatarEmoteCommand } from '../../../../protocol/decentraland/sdk/components/avatar_emote_command.gen' +import { PBAvatarBase } from '../../../../protocol/decentraland/sdk/components/avatar_base.gen' +import { PBPlayerIdentityData } from '../../../../protocol/decentraland/sdk/components/player_identity_data.gen' +import { PBAvatarEquippedData } from '../../../../protocol/decentraland/sdk/components/avatar_equipped_data.gen' +import { NewProfileForRenderer } from 'lib/decentraland/profiles/transformations' + +const MAX_ENTITY_VERSION = 0xffff +const AVATAR_RESERVED_ENTITY_NUMBER = { from: 10, to: 200 } + +const ComponentIds = { + TRANSFORM: 1, + AVATAR_BASE: 1087, + AVATAR_EMOTE_COMMAND: 1088, + PLAYER_IDENTITY_DATA: 1089, + AVATAR_EQUIPPED_DATA: 1091 +} + +export function createTinyEcs() { + const avatarEntity = new Map() + const entities: Map = new Map() + const componentsTimestamp: Map> = 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 updateAvatarEmoteCommand(entity: Entity, data): Uint8Array { + const component = getComponentTimestamp(ComponentIds.AVATAR_EMOTE_COMMAND) + + // TODO: convert the data + const componentValue = data + const writer = PBAvatarEmoteCommand.encode(componentValue) + const buffer = new Uint8Array(writer.finish(), 0, writer.len) + + const timestamp = (component.get(entity)?.ts || -1) + 1 + AppendValueOperation.write(entity, timestamp, ComponentIds.AVATAR_EMOTE_COMMAND, buffer, crdtReusableBuffer) + + // update timestamp + component.set(entity, { ts: timestamp }) + + return crdtReusableBuffer.toCopiedBinary() + } + + function updateAvatarBase(entity: Entity, data: NewProfileForRenderer): Uint8Array { + const component = getComponentTimestamp(ComponentIds.AVATAR_BASE) + + // TODO: convert the data + const componentValue = data as any + const writer = PBAvatarBase.encode(componentValue) + const buffer = new Uint8Array(writer.finish(), 0, writer.len) + + const timestamp = (component.get(entity)?.ts || -1) + 1 + PutComponentOperation.write(entity, timestamp, ComponentIds.AVATAR_BASE, buffer, crdtReusableBuffer) + + // update timestamp + component.set(entity, { ts: timestamp }) + + return crdtReusableBuffer.toCopiedBinary() + } + + function updatePlayerIdentityData(entity: Entity, data): Uint8Array { + const component = getComponentTimestamp(ComponentIds.PLAYER_IDENTITY_DATA) + + // TODO: convert the data + const componentValue = data + const writer = PBPlayerIdentityData.encode(componentValue) + const buffer = new Uint8Array(writer.finish(), 0, writer.len) + + const timestamp = (component.get(entity)?.ts || -1) + 1 + PutComponentOperation.write(entity, timestamp, ComponentIds.PLAYER_IDENTITY_DATA, buffer, crdtReusableBuffer) + + // update timestamp + component.set(entity, { ts: timestamp }) + + return crdtReusableBuffer.toCopiedBinary() + } + + function updateAvatarEquippedData(entity: Entity, data): Uint8Array { + const component = getComponentTimestamp(ComponentIds.AVATAR_EQUIPPED_DATA) + + // TODO: convert the data + const componentValue = data + const writer = PBAvatarEquippedData.encode(componentValue) + const buffer = new Uint8Array(writer.finish(), 0, writer.len) + + const timestamp = (component.get(entity)?.ts || -1) + 1 + PutComponentOperation.write(entity, timestamp, ComponentIds.AVATAR_EQUIPPED_DATA, buffer, crdtReusableBuffer) + + // update timestamp + component.set(entity, { ts: timestamp }) + + return crdtReusableBuffer.toCopiedBinary() + } + + function updateAvatarTransform(entity: Entity, data: rfc4.Position): { data: Uint8Array; ts: number } { + const component = getComponentTimestamp(ComponentIds.TRANSFORM) + transformReusableBuffer.resetBuffer() + + transformReusableBuffer.setFloat32(0, data.positionX) + transformReusableBuffer.setFloat32(4, data.positionY) + transformReusableBuffer.setFloat32(8, data.positionZ) + + // TODO: See convert EULER rotation to quaternion ? + transformReusableBuffer.setFloat32(12, data.rotationX) + transformReusableBuffer.setFloat32(16, data.rotationY) + transformReusableBuffer.setFloat32(20, data.rotationZ) + transformReusableBuffer.setFloat32(24, 1.0) + + transformReusableBuffer.setFloat32(28, 1.0) + transformReusableBuffer.setFloat32(32, 1.0) + transformReusableBuffer.setFloat32(36, 1.0) + transformReusableBuffer.setUint32(40, 0) + + const timestamp = (component.get(entity)?.ts || -1) + 1 + PutComponentOperation.write( + entity, + timestamp, + ComponentIds.TRANSFORM, + transformReusableBuffer.toBinary(), + crdtReusableBuffer + ) + return { ts: timestamp, data: crdtReusableBuffer.toCopiedBinary() } + } + + return { + ensureAvatarEntityId, + removeAvatarEntityId, + + updateAvatarTransform, + updateAvatarBase, + updateAvatarEquippedData, + updateAvatarEmoteCommand, + updatePlayerIdentityData + } +} diff --git a/browser-interface/packages/shared/apis/host/sdk7/avatar/index.ts b/browser-interface/packages/shared/apis/host/sdk7/avatar/index.ts new file mode 100644 index 0000000000..a66c36fc89 --- /dev/null +++ b/browser-interface/packages/shared/apis/host/sdk7/avatar/index.ts @@ -0,0 +1,54 @@ +import { avatarMessageObservable } from '../../../../comms/peers' +import mitt from 'mitt' +import { createTinyEcs } from './ecs' +import { Entity } from '../engine/entity' + +export type AvatarSdk7Message = { + ChangePosition: { + parcel: { x: number; z: number } + entity: Entity + ts: number + data: Uint8Array + } + + BinaryMessage: Uint8Array + + RemoveAvatar: { + entity: Entity + data: Uint8Array + } +} + +const avatarEcs = createTinyEcs() +export const avatarSdk7MessageObservable = mitt() + +avatarMessageObservable.add((evt) => { + const avatarEntityId = avatarEcs.ensureAvatarEntityId(evt.userId) + if (evt.type === 'USER_DATA') { + if (evt.data.position) { + const message = avatarEcs.updateAvatarTransform(avatarEntityId, evt.data.position) + avatarSdk7MessageObservable.emit('ChangePosition', { + parcel: { x: Math.floor(evt.data.position.positionX), z: Math.floor(evt.data.position.positionZ) }, + entity: avatarEntityId, + data: message.data, + ts: message.ts + }) + } + + // if (evt.profile) { + // const avatarBase = avatarEcs.updateAvatarBase(avatarEntityId, evt.data) + // avatarSdk7MessageObservable.emit('BinaryMessage', avatarBase) + + // if (evt.profile.avatar) { + // const avatarEquippedData = avatarEcs.updateAvatarEquippedData(avatarEntityId, evt.data) + // avatarSdk7MessageObservable.emit('BinaryMessage', avatarEquippedData) + // } + // } + } else if (evt.type === 'USER_EXPRESSION') { + const avatarEmoteCommand = avatarEcs.updateAvatarEmoteCommand(avatarEntityId, evt) + avatarSdk7MessageObservable.emit('BinaryMessage', avatarEmoteCommand) + } else if (evt.type === 'USER_REMOVED') { + const avatarRemoveEntity = avatarEcs.removeAvatarEntityId(evt.userId) + avatarSdk7MessageObservable.emit('RemoveAvatar', { entity: avatarEntityId, data: avatarRemoveEntity }) + } +}) diff --git a/browser-interface/packages/shared/world/SceneWorker.ts b/browser-interface/packages/shared/world/SceneWorker.ts index 540608d6f6..c9d4cdb5ff 100644 --- a/browser-interface/packages/shared/world/SceneWorker.ts +++ b/browser-interface/packages/shared/world/SceneWorker.ts @@ -165,6 +165,7 @@ export class SceneWorker { sdk7: IS_SDK7, scenePort, rpcSceneControllerService, + avatarEntityInsideScene: new Map(), sceneData: { isPortableExperience: false, useFPSThrottling: false, diff --git a/browser-interface/test/unit/RestrictedActions.test.tsx b/browser-interface/test/unit/RestrictedActions.test.tsx index 1ef9a996e0..8934e8a36e 100644 --- a/browser-interface/test/unit/RestrictedActions.test.tsx +++ b/browser-interface/test/unit/RestrictedActions.test.tsx @@ -15,7 +15,7 @@ describe('RestrictedActions tests', () => { beforeEach(() => { sinon.reset() sinon.restore() - setUnityInstance({ Teleport: () => { }, TriggerSelfUserExpression: () => { } } as any) + setUnityInstance({ Teleport: () => {}, TriggerSelfUserExpression: () => {} } as any) buildStore() }) @@ -139,6 +139,7 @@ describe('RestrictedActions tests', () => { permissionGranted: new Set(permissions), subscribedEvents: new Set(), events: [], + avatarEntityInsideScene: new Map(), sendProtoSceneEvent() { throw new Error('not implemented') }, @@ -152,7 +153,7 @@ describe('RestrictedActions tests', () => { initialEntitiesTick0: Uint8Array.of(), readFile(_path) { throw new Error('not implemented') - }, + } } } From 24dc718a7814a8416cded222dfc4e4d6668c4f26 Mon Sep 17 00:00:00 2001 From: Lean Mendoza Date: Wed, 20 Sep 2023 16:35:49 -0300 Subject: [PATCH 3/5] wip --- .../packages/shared/apis/host/EngineAPI.ts | 77 +++++++---- .../shared/apis/host/sdk7/avatar/ecs.ts | 125 ++++++++++-------- .../shared/apis/host/sdk7/avatar/index.ts | 15 +-- 3 files changed, 123 insertions(+), 94 deletions(-) diff --git a/browser-interface/packages/shared/apis/host/EngineAPI.ts b/browser-interface/packages/shared/apis/host/EngineAPI.ts index 61b2bed88f..18d9054e3c 100644 --- a/browser-interface/packages/shared/apis/host/EngineAPI.ts +++ b/browser-interface/packages/shared/apis/host/EngineAPI.ts @@ -6,40 +6,61 @@ import { EngineApiServiceDefinition } from 'shared/protocol/decentraland/kernel/ import type { PortContext } from './context' import { avatarSdk7MessageObservable } from './sdk7/avatar' +import { DeleteComponent } from './sdk7/serialization/crdt/deleteComponent' +import { ReadWriteByteBuffer } from './sdk7/serialization/ByteBuffer' +import { Sdk7ComponentIds } from './sdk7/avatar/ecs' + +function getParcelNumber(x: number, z: number) { + return z * 100e8 + x +} export function registerEngineApiServiceServerImplementation(port: RpcServerPort) { codegen.registerService( port, EngineApiServiceDefinition, async (port, ctx): Promise> => { - let avatarUpdates: Uint8Array[] = [] - avatarSdk7MessageObservable.on('BinaryMessage', (message) => { - avatarUpdates.push(message) - }) - - avatarSdk7MessageObservable.on('RemoveAvatar', (message) => { - avatarUpdates.push(message.data) - ctx.avatarEntityInsideScene.delete(message.entity) - }) - - avatarSdk7MessageObservable.on('ChangePosition', (message) => { - // TODO: Define how to know if an entity is inside the scene - const isInsideScene = true - - const wasInsideScene = ctx.avatarEntityInsideScene.get(message.entity) || false - - if (isInsideScene) { - avatarUpdates.push(message.data) + let sdk7AvatarUpdates: Uint8Array[] = [] + + if (ctx.sdk7) { + const tempReusableBuffer = new ReadWriteByteBuffer() + const parcels: Set = 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)) + }) + } - if (!wasInsideScene) { - ctx.avatarEntityInsideScene.set(message.entity, true) + console.log({ parcels }) + + 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(message.data) + + 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()) } - } else if (wasInsideScene) { - ctx.avatarEntityInsideScene.set(message.entity, false) - - // TODO: Send delete transform - } - }) + }) + } return { async sendBatch(_req: ManyEntityAction, ctx) { @@ -76,8 +97,8 @@ export function registerEngineApiServiceServerImplementation(port: RpcServerPort payload: req.data }) - const avatarStates = avatarUpdates - avatarUpdates = [] + const avatarStates = sdk7AvatarUpdates + sdk7AvatarUpdates = [] return { data: [ret.payload, ...avatarStates] } }, diff --git a/browser-interface/packages/shared/apis/host/sdk7/avatar/ecs.ts b/browser-interface/packages/shared/apis/host/sdk7/avatar/ecs.ts index 66b55edf53..2a7b75dffe 100644 --- a/browser-interface/packages/shared/apis/host/sdk7/avatar/ecs.ts +++ b/browser-interface/packages/shared/apis/host/sdk7/avatar/ecs.ts @@ -12,11 +12,12 @@ import { PBAvatarBase } from '../../../../protocol/decentraland/sdk/components/a import { PBPlayerIdentityData } from '../../../../protocol/decentraland/sdk/components/player_identity_data.gen' import { PBAvatarEquippedData } from '../../../../protocol/decentraland/sdk/components/avatar_equipped_data.gen' import { NewProfileForRenderer } from 'lib/decentraland/profiles/transformations' +import { ReceiveUserExpressionMessage } from 'shared/comms/interface/types' const MAX_ENTITY_VERSION = 0xffff const AVATAR_RESERVED_ENTITY_NUMBER = { from: 10, to: 200 } -const ComponentIds = { +export const Sdk7ComponentIds = { TRANSFORM: 1, AVATAR_BASE: 1087, AVATAR_EMOTE_COMMAND: 1088, @@ -95,50 +96,20 @@ export function createTinyEcs() { return componentsTimestamp.get(componentId)! } - function updateAvatarEmoteCommand(entity: Entity, data): Uint8Array { - const component = getComponentTimestamp(ComponentIds.AVATAR_EMOTE_COMMAND) + function updateAvatarEmoteCommand(entity: Entity, data: ReceiveUserExpressionMessage): Uint8Array { + const component = getComponentTimestamp(Sdk7ComponentIds.AVATAR_EMOTE_COMMAND) - // TODO: convert the data - const componentValue = data - const writer = PBAvatarEmoteCommand.encode(componentValue) - const buffer = new Uint8Array(writer.finish(), 0, writer.len) - - const timestamp = (component.get(entity)?.ts || -1) + 1 - AppendValueOperation.write(entity, timestamp, ComponentIds.AVATAR_EMOTE_COMMAND, buffer, crdtReusableBuffer) - - // update timestamp - component.set(entity, { ts: timestamp }) - - return crdtReusableBuffer.toCopiedBinary() - } - - function updateAvatarBase(entity: Entity, data: NewProfileForRenderer): Uint8Array { - const component = getComponentTimestamp(ComponentIds.AVATAR_BASE) - - // TODO: convert the data - const componentValue = data as any - const writer = PBAvatarBase.encode(componentValue) - const buffer = new Uint8Array(writer.finish(), 0, writer.len) - - const timestamp = (component.get(entity)?.ts || -1) + 1 - PutComponentOperation.write(entity, timestamp, ComponentIds.AVATAR_BASE, buffer, crdtReusableBuffer) - - // update timestamp - component.set(entity, { ts: timestamp }) - - return crdtReusableBuffer.toCopiedBinary() - } - - function updatePlayerIdentityData(entity: Entity, data): Uint8Array { - const component = getComponentTimestamp(ComponentIds.PLAYER_IDENTITY_DATA) + const writer = PBAvatarEmoteCommand.encode({ + emoteCommand: { + emoteUrn: data.expressionId, + loop: false // TODO: how to know if is loopable + } + }) - // TODO: convert the data - const componentValue = data - const writer = PBPlayerIdentityData.encode(componentValue) const buffer = new Uint8Array(writer.finish(), 0, writer.len) const timestamp = (component.get(entity)?.ts || -1) + 1 - PutComponentOperation.write(entity, timestamp, ComponentIds.PLAYER_IDENTITY_DATA, buffer, crdtReusableBuffer) + AppendValueOperation.write(entity, timestamp, Sdk7ComponentIds.AVATAR_EMOTE_COMMAND, buffer, crdtReusableBuffer) // update timestamp component.set(entity, { ts: timestamp }) @@ -146,25 +117,67 @@ export function createTinyEcs() { return crdtReusableBuffer.toCopiedBinary() } - function updateAvatarEquippedData(entity: Entity, data): Uint8Array { - const component = getComponentTimestamp(ComponentIds.AVATAR_EQUIPPED_DATA) + 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 || -1) + 1 + PutComponentOperation.write(entity, timestamp, Sdk7ComponentIds.PLAYER_IDENTITY_DATA, buffer, crdtReusableBuffer) + playerIdentityComponent.set(entity, { ts: timestamp }) + + msgs.push(crdtReusableBuffer.toCopiedBinary()) + } - // TODO: convert the data - const componentValue = data - const writer = PBAvatarEquippedData.encode(componentValue) - const buffer = new Uint8Array(writer.finish(), 0, writer.len) + // 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 || -1) + 1 + PutComponentOperation.write(entity, timestamp, Sdk7ComponentIds.AVATAR_BASE, buffer, crdtReusableBuffer) + avatarBaseComponent.set(entity, { ts: timestamp }) + + msgs.push(crdtReusableBuffer.toCopiedBinary()) + } - const timestamp = (component.get(entity)?.ts || -1) + 1 - PutComponentOperation.write(entity, timestamp, ComponentIds.AVATAR_EQUIPPED_DATA, buffer, crdtReusableBuffer) + // Update avatar equipped data + { + crdtReusableBuffer.resetBuffer() - // update timestamp - component.set(entity, { ts: timestamp }) + 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 || -1) + 1 + PutComponentOperation.write(entity, timestamp, Sdk7ComponentIds.AVATAR_EQUIPPED_DATA, buffer, crdtReusableBuffer) + avatarEquippedComponent.set(entity, { ts: timestamp }) - return crdtReusableBuffer.toCopiedBinary() + msgs.push(crdtReusableBuffer.toCopiedBinary()) + } + return msgs } function updateAvatarTransform(entity: Entity, data: rfc4.Position): { data: Uint8Array; ts: number } { - const component = getComponentTimestamp(ComponentIds.TRANSFORM) + const component = getComponentTimestamp(Sdk7ComponentIds.TRANSFORM) transformReusableBuffer.resetBuffer() transformReusableBuffer.setFloat32(0, data.positionX) @@ -186,7 +199,7 @@ export function createTinyEcs() { PutComponentOperation.write( entity, timestamp, - ComponentIds.TRANSFORM, + Sdk7ComponentIds.TRANSFORM, transformReusableBuffer.toBinary(), crdtReusableBuffer ) @@ -198,9 +211,7 @@ export function createTinyEcs() { removeAvatarEntityId, updateAvatarTransform, - updateAvatarBase, - updateAvatarEquippedData, - updateAvatarEmoteCommand, - updatePlayerIdentityData + updateProfile, + updateAvatarEmoteCommand } } diff --git a/browser-interface/packages/shared/apis/host/sdk7/avatar/index.ts b/browser-interface/packages/shared/apis/host/sdk7/avatar/index.ts index a66c36fc89..6b9fa2826c 100644 --- a/browser-interface/packages/shared/apis/host/sdk7/avatar/index.ts +++ b/browser-interface/packages/shared/apis/host/sdk7/avatar/index.ts @@ -35,15 +35,12 @@ avatarMessageObservable.add((evt) => { }) } - // if (evt.profile) { - // const avatarBase = avatarEcs.updateAvatarBase(avatarEntityId, evt.data) - // avatarSdk7MessageObservable.emit('BinaryMessage', avatarBase) - - // if (evt.profile.avatar) { - // const avatarEquippedData = avatarEcs.updateAvatarEquippedData(avatarEntityId, evt.data) - // avatarSdk7MessageObservable.emit('BinaryMessage', avatarEquippedData) - // } - // } + if (evt.profile) { + const avatarBase = avatarEcs.updateProfile(avatarEntityId, evt.profile) + for (const msg of avatarBase) { + avatarSdk7MessageObservable.emit('BinaryMessage', msg) + } + } } else if (evt.type === 'USER_EXPRESSION') { const avatarEmoteCommand = avatarEcs.updateAvatarEmoteCommand(avatarEntityId, evt) avatarSdk7MessageObservable.emit('BinaryMessage', avatarEmoteCommand) From 54f2dc5f42570bc4f655ee9e3290e81a335ad80d Mon Sep 17 00:00:00 2001 From: Lean Mendoza Date: Wed, 20 Sep 2023 17:59:22 -0300 Subject: [PATCH 4/5] position update works --- .../packages/shared/apis/host/EngineAPI.ts | 18 ++- .../packages/shared/apis/host/context.ts | 10 +- .../apis/host/{sdk7 => runtime7}/README.md | 0 .../host/{sdk7 => runtime7}/avatar/ecs.ts | 127 ++++++++++-------- .../host/{sdk7 => runtime7}/avatar/index.ts | 28 ++-- .../host/{sdk7 => runtime7}/engine/entity.ts | 0 .../serialization/ByteBuffer/index.ts | 0 .../serialization/crdt/appendValue.ts | 0 .../serialization/crdt/crdtMessageProtocol.ts | 0 .../serialization/crdt/deleteComponent.ts | 0 .../serialization/crdt/deleteEntity.ts | 0 .../serialization/crdt/index.ts | 0 .../serialization/crdt/message.ts | 0 .../serialization/crdt/putComponent.ts | 0 .../serialization/crdt/types.ts | 0 .../host/runtime7/serialization/transform.ts | 39 ++++++ 16 files changed, 143 insertions(+), 79 deletions(-) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/README.md (100%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/avatar/ecs.ts (68%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/avatar/index.ts (58%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/engine/entity.ts (100%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/serialization/ByteBuffer/index.ts (100%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/serialization/crdt/appendValue.ts (100%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/serialization/crdt/crdtMessageProtocol.ts (100%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/serialization/crdt/deleteComponent.ts (100%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/serialization/crdt/deleteEntity.ts (100%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/serialization/crdt/index.ts (100%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/serialization/crdt/message.ts (100%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/serialization/crdt/putComponent.ts (100%) rename browser-interface/packages/shared/apis/host/{sdk7 => runtime7}/serialization/crdt/types.ts (100%) create mode 100644 browser-interface/packages/shared/apis/host/runtime7/serialization/transform.ts diff --git a/browser-interface/packages/shared/apis/host/EngineAPI.ts b/browser-interface/packages/shared/apis/host/EngineAPI.ts index 18d9054e3c..02f133df13 100644 --- a/browser-interface/packages/shared/apis/host/EngineAPI.ts +++ b/browser-interface/packages/shared/apis/host/EngineAPI.ts @@ -5,10 +5,11 @@ 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 { avatarSdk7MessageObservable } from './sdk7/avatar' -import { DeleteComponent } from './sdk7/serialization/crdt/deleteComponent' -import { ReadWriteByteBuffer } from './sdk7/serialization/ByteBuffer' -import { Sdk7ComponentIds } from './sdk7/avatar/ecs' +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 @@ -31,7 +32,12 @@ export function registerEngineApiServiceServerImplementation(port: RpcServerPort }) } - console.log({ parcels }) + 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) @@ -47,7 +53,7 @@ export function registerEngineApiServiceServerImplementation(port: RpcServerPort ctx.sceneData.isGlobalScene || parcels.has(getParcelNumber(message.parcel.x, message.parcel.z)) const wasInsideScene = ctx.avatarEntityInsideScene.get(message.entity) || false if (isInsideScene) { - sdk7AvatarUpdates.push(message.data) + sdk7AvatarUpdates.push(buildAvatarTransformMessage(message.entity, message.ts, message.data, offset)) if (!wasInsideScene) { ctx.avatarEntityInsideScene.set(message.entity, true) diff --git a/browser-interface/packages/shared/apis/host/context.ts b/browser-interface/packages/shared/apis/host/context.ts index 7b234b36aa..4525262dea 100644 --- a/browser-interface/packages/shared/apis/host/context.ts +++ b/browser-interface/packages/shared/apis/host/context.ts @@ -1,12 +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 { Entity } from './sdk7/engine/entity' +import type { LoadableScene } from 'shared/types' +import type { Entity } from './runtime7/engine/entity' type WithRequired = T & { [P in K]-?: T[P] } diff --git a/browser-interface/packages/shared/apis/host/sdk7/README.md b/browser-interface/packages/shared/apis/host/runtime7/README.md similarity index 100% rename from browser-interface/packages/shared/apis/host/sdk7/README.md rename to browser-interface/packages/shared/apis/host/runtime7/README.md diff --git a/browser-interface/packages/shared/apis/host/sdk7/avatar/ecs.ts b/browser-interface/packages/shared/apis/host/runtime7/avatar/ecs.ts similarity index 68% rename from browser-interface/packages/shared/apis/host/sdk7/avatar/ecs.ts rename to browser-interface/packages/shared/apis/host/runtime7/avatar/ecs.ts index 2a7b75dffe..30af8d253f 100644 --- a/browser-interface/packages/shared/apis/host/sdk7/avatar/ecs.ts +++ b/browser-interface/packages/shared/apis/host/runtime7/avatar/ecs.ts @@ -1,18 +1,14 @@ +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 { PutComponentOperation } from '../serialization/crdt/putComponent' import { AppendValueOperation } from '../serialization/crdt/appendValue' import { DeleteEntity } from '../serialization/crdt/deleteEntity' -// import * as rfc4 from 'shared/protocol/decentraland/kernel/comms/rfc4/comms.gen' -// import { PBAvatarEmoteCommand } from 'shared/protocol/out-ts/decentraland/sdk/components/avatar_emote_command.gen.ts' - -import * as rfc4 from '../../../../protocol/decentraland/kernel/comms/rfc4/comms.gen' -import { PBAvatarEmoteCommand } from '../../../../protocol/decentraland/sdk/components/avatar_emote_command.gen' -import { PBAvatarBase } from '../../../../protocol/decentraland/sdk/components/avatar_base.gen' -import { PBPlayerIdentityData } from '../../../../protocol/decentraland/sdk/components/player_identity_data.gen' -import { PBAvatarEquippedData } from '../../../../protocol/decentraland/sdk/components/avatar_equipped_data.gen' -import { NewProfileForRenderer } from 'lib/decentraland/profiles/transformations' -import { ReceiveUserExpressionMessage } from 'shared/comms/interface/types' +import { PutComponentOperation } from '../serialization/crdt/putComponent' const MAX_ENTITY_VERSION = 0xffff const AVATAR_RESERVED_ENTITY_NUMBER = { from: 10, to: 200 } @@ -28,7 +24,7 @@ export const Sdk7ComponentIds = { export function createTinyEcs() { const avatarEntity = new Map() const entities: Map = new Map() - const componentsTimestamp: Map> = new Map() + const componentsTimestamp: Map> = new Map() const crdtReusableBuffer = new ReadWriteByteBuffer() const transformReusableBuffer = new ReadWriteByteBuffer() @@ -96,8 +92,8 @@ export function createTinyEcs() { return componentsTimestamp.get(componentId)! } - function updateAvatarEmoteCommand(entity: Entity, data: ReceiveUserExpressionMessage): Uint8Array { - const component = getComponentTimestamp(Sdk7ComponentIds.AVATAR_EMOTE_COMMAND) + function appendAvatarEmoteCommand(entity: Entity, data: ReceiveUserExpressionMessage): Uint8Array { + const avatarEmoteCommandComponent = getComponentTimestamp(Sdk7ComponentIds.AVATAR_EMOTE_COMMAND) const writer = PBAvatarEmoteCommand.encode({ emoteCommand: { @@ -108,11 +104,10 @@ export function createTinyEcs() { const buffer = new Uint8Array(writer.finish(), 0, writer.len) - const timestamp = (component.get(entity)?.ts || -1) + 1 + const timestamp = (avatarEmoteCommandComponent.get(entity)?.ts || 0) + 1 AppendValueOperation.write(entity, timestamp, Sdk7ComponentIds.AVATAR_EMOTE_COMMAND, buffer, crdtReusableBuffer) - // update timestamp - component.set(entity, { ts: timestamp }) + avatarEmoteCommandComponent.set(entity, { ts: timestamp }) return crdtReusableBuffer.toCopiedBinary() } @@ -132,11 +127,12 @@ export function createTinyEcs() { isGuest: data.hasConnectedWeb3 }) const buffer = new Uint8Array(writer.finish(), 0, writer.len) - const timestamp = (playerIdentityComponent.get(entity)?.ts || -1) + 1 + const timestamp = (playerIdentityComponent.get(entity)?.ts || 0) + 1 PutComponentOperation.write(entity, timestamp, Sdk7ComponentIds.PLAYER_IDENTITY_DATA, buffer, crdtReusableBuffer) - playerIdentityComponent.set(entity, { ts: timestamp }) - msgs.push(crdtReusableBuffer.toCopiedBinary()) + const messageData = crdtReusableBuffer.toCopiedBinary() + playerIdentityComponent.set(entity, { ts: timestamp, lastMessageData: messageData }) + msgs.push(messageData) } // Update avatar base @@ -151,11 +147,12 @@ export function createTinyEcs() { name: data.name }) const buffer = new Uint8Array(writer.finish(), 0, writer.len) - const timestamp = (avatarBaseComponent.get(entity)?.ts || -1) + 1 + const timestamp = (avatarBaseComponent.get(entity)?.ts || 0) + 1 PutComponentOperation.write(entity, timestamp, Sdk7ComponentIds.AVATAR_BASE, buffer, crdtReusableBuffer) - avatarBaseComponent.set(entity, { ts: timestamp }) - msgs.push(crdtReusableBuffer.toCopiedBinary()) + const messageData = crdtReusableBuffer.toCopiedBinary() + avatarBaseComponent.set(entity, { ts: timestamp, lastMessageData: messageData }) + msgs.push(messageData) } // Update avatar equipped data @@ -167,51 +164,69 @@ export function createTinyEcs() { emotesUrns: (data.avatar.emotes || []).map(($) => $.urn) }) const buffer = new Uint8Array(writer.finish(), 0, writer.len) - const timestamp = (avatarEquippedComponent.get(entity)?.ts || -1) + 1 + const timestamp = (avatarEquippedComponent.get(entity)?.ts || 0) + 1 PutComponentOperation.write(entity, timestamp, Sdk7ComponentIds.AVATAR_EQUIPPED_DATA, buffer, crdtReusableBuffer) - avatarEquippedComponent.set(entity, { ts: timestamp }) - msgs.push(crdtReusableBuffer.toCopiedBinary()) + const messageData = crdtReusableBuffer.toCopiedBinary() + avatarEquippedComponent.set(entity, { ts: timestamp, lastMessageData: messageData }) + msgs.push(messageData) } return msgs } - function updateAvatarTransform(entity: Entity, data: rfc4.Position): { data: Uint8Array; ts: number } { - const component = getComponentTimestamp(Sdk7ComponentIds.TRANSFORM) - transformReusableBuffer.resetBuffer() - - transformReusableBuffer.setFloat32(0, data.positionX) - transformReusableBuffer.setFloat32(4, data.positionY) - transformReusableBuffer.setFloat32(8, data.positionZ) - - // TODO: See convert EULER rotation to quaternion ? - transformReusableBuffer.setFloat32(12, data.rotationX) - transformReusableBuffer.setFloat32(16, data.rotationY) - transformReusableBuffer.setFloat32(20, data.rotationZ) - transformReusableBuffer.setFloat32(24, 1.0) - - transformReusableBuffer.setFloat32(28, 1.0) - transformReusableBuffer.setFloat32(32, 1.0) - transformReusableBuffer.setFloat32(36, 1.0) - transformReusableBuffer.setUint32(40, 0) - - const timestamp = (component.get(entity)?.ts || -1) + 1 - PutComponentOperation.write( - entity, - timestamp, - Sdk7ComponentIds.TRANSFORM, - transformReusableBuffer.toBinary(), - crdtReusableBuffer - ) - return { ts: timestamp, data: crdtReusableBuffer.toCopiedBinary() } + 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, - updateAvatarTransform, + computeNextAvatarTransformTimestamp, updateProfile, - updateAvatarEmoteCommand + appendAvatarEmoteCommand, + + getState } } diff --git a/browser-interface/packages/shared/apis/host/sdk7/avatar/index.ts b/browser-interface/packages/shared/apis/host/runtime7/avatar/index.ts similarity index 58% rename from browser-interface/packages/shared/apis/host/sdk7/avatar/index.ts rename to browser-interface/packages/shared/apis/host/runtime7/avatar/index.ts index 6b9fa2826c..c79f4af6fa 100644 --- a/browser-interface/packages/shared/apis/host/sdk7/avatar/index.ts +++ b/browser-interface/packages/shared/apis/host/runtime7/avatar/index.ts @@ -1,14 +1,15 @@ -import { avatarMessageObservable } from '../../../../comms/peers' import mitt from 'mitt' -import { createTinyEcs } from './ecs' +import { avatarMessageObservable } from '../../../../comms/peers' +import * as rfc4 from '../../../../protocol/decentraland/kernel/comms/rfc4/comms.gen' import { Entity } from '../engine/entity' +import { createTinyEcs } from './ecs' export type AvatarSdk7Message = { ChangePosition: { parcel: { x: number; z: number } entity: Entity ts: number - data: Uint8Array + data: rfc4.Position } BinaryMessage: Uint8Array @@ -19,33 +20,36 @@ export type AvatarSdk7Message = { } } -const avatarEcs = createTinyEcs() +export const avatarSdk7Ecs = createTinyEcs() export const avatarSdk7MessageObservable = mitt() avatarMessageObservable.add((evt) => { - const avatarEntityId = avatarEcs.ensureAvatarEntityId(evt.userId) + const avatarEntityId = avatarSdk7Ecs.ensureAvatarEntityId(evt.userId) if (evt.type === 'USER_DATA') { if (evt.data.position) { - const message = avatarEcs.updateAvatarTransform(avatarEntityId, evt.data.position) + const timestamp = avatarSdk7Ecs.computeNextAvatarTransformTimestamp(avatarEntityId) avatarSdk7MessageObservable.emit('ChangePosition', { - parcel: { x: Math.floor(evt.data.position.positionX), z: Math.floor(evt.data.position.positionZ) }, + parcel: { + x: Math.floor(evt.data.position.positionX / 16.0), + z: Math.floor(evt.data.position.positionZ / 16.0) + }, entity: avatarEntityId, - data: message.data, - ts: message.ts + data: evt.data.position, + ts: timestamp }) } if (evt.profile) { - const avatarBase = avatarEcs.updateProfile(avatarEntityId, evt.profile) + const avatarBase = avatarSdk7Ecs.updateProfile(avatarEntityId, evt.profile) for (const msg of avatarBase) { avatarSdk7MessageObservable.emit('BinaryMessage', msg) } } } else if (evt.type === 'USER_EXPRESSION') { - const avatarEmoteCommand = avatarEcs.updateAvatarEmoteCommand(avatarEntityId, evt) + const avatarEmoteCommand = avatarSdk7Ecs.appendAvatarEmoteCommand(avatarEntityId, evt) avatarSdk7MessageObservable.emit('BinaryMessage', avatarEmoteCommand) } else if (evt.type === 'USER_REMOVED') { - const avatarRemoveEntity = avatarEcs.removeAvatarEntityId(evt.userId) + const avatarRemoveEntity = avatarSdk7Ecs.removeAvatarEntityId(evt.userId) avatarSdk7MessageObservable.emit('RemoveAvatar', { entity: avatarEntityId, data: avatarRemoveEntity }) } }) diff --git a/browser-interface/packages/shared/apis/host/sdk7/engine/entity.ts b/browser-interface/packages/shared/apis/host/runtime7/engine/entity.ts similarity index 100% rename from browser-interface/packages/shared/apis/host/sdk7/engine/entity.ts rename to browser-interface/packages/shared/apis/host/runtime7/engine/entity.ts diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/ByteBuffer/index.ts b/browser-interface/packages/shared/apis/host/runtime7/serialization/ByteBuffer/index.ts similarity index 100% rename from browser-interface/packages/shared/apis/host/sdk7/serialization/ByteBuffer/index.ts rename to browser-interface/packages/shared/apis/host/runtime7/serialization/ByteBuffer/index.ts diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/appendValue.ts b/browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/appendValue.ts similarity index 100% rename from browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/appendValue.ts rename to browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/appendValue.ts diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/crdtMessageProtocol.ts b/browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/crdtMessageProtocol.ts similarity index 100% rename from browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/crdtMessageProtocol.ts rename to browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/crdtMessageProtocol.ts diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteComponent.ts b/browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/deleteComponent.ts similarity index 100% rename from browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteComponent.ts rename to browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/deleteComponent.ts diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteEntity.ts b/browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/deleteEntity.ts similarity index 100% rename from browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/deleteEntity.ts rename to browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/deleteEntity.ts diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/index.ts b/browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/index.ts similarity index 100% rename from browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/index.ts rename to browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/index.ts diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/message.ts b/browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/message.ts similarity index 100% rename from browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/message.ts rename to browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/message.ts diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/putComponent.ts b/browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/putComponent.ts similarity index 100% rename from browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/putComponent.ts rename to browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/putComponent.ts diff --git a/browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/types.ts b/browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/types.ts similarity index 100% rename from browser-interface/packages/shared/apis/host/sdk7/serialization/crdt/types.ts rename to browser-interface/packages/shared/apis/host/runtime7/serialization/crdt/types.ts diff --git a/browser-interface/packages/shared/apis/host/runtime7/serialization/transform.ts b/browser-interface/packages/shared/apis/host/runtime7/serialization/transform.ts new file mode 100644 index 0000000000..1bfd8ebc22 --- /dev/null +++ b/browser-interface/packages/shared/apis/host/runtime7/serialization/transform.ts @@ -0,0 +1,39 @@ +import { Vector3 } from 'lib/math/Vector3' +import * as rfc4 from '../../../../protocol/decentraland/kernel/comms/rfc4/comms.gen' +import { Entity } from '../engine/entity' +import { Sdk7ComponentIds } from './../avatar/ecs' +import { PutComponentOperation } from './../serialization/crdt/putComponent' +import { ReadWriteByteBuffer } from './ByteBuffer' + +const crdtReusableMessage = new ReadWriteByteBuffer() +const transformReusableMessage = new ReadWriteByteBuffer() + +export function buildAvatarTransformMessage(entity: Entity, timestamp: number, data: rfc4.Position, offset: Vector3) { + transformReusableMessage.resetBuffer() + transformReusableMessage.incrementWriteOffset(44) + + transformReusableMessage.setFloat32(0, data.positionX - offset.x) + transformReusableMessage.setFloat32(4, data.positionY) + transformReusableMessage.setFloat32(8, data.positionZ - offset.z) + + transformReusableMessage.setFloat32(12, data.rotationX) + transformReusableMessage.setFloat32(16, data.rotationY) + transformReusableMessage.setFloat32(20, data.rotationZ) + transformReusableMessage.setFloat32(24, data.rotationW) + + transformReusableMessage.setFloat32(28, 1.0) + transformReusableMessage.setFloat32(32, 1.0) + transformReusableMessage.setFloat32(36, 1.0) + transformReusableMessage.setUint32(40, 0) + + crdtReusableMessage.resetBuffer() + PutComponentOperation.write( + entity, + timestamp, + Sdk7ComponentIds.TRANSFORM, + transformReusableMessage.toBinary(), + crdtReusableMessage + ) + + return crdtReusableMessage.toCopiedBinary() +} From 558a484a37a01f44df6c0c42e5eadc4bd75ba35a Mon Sep 17 00:00:00 2001 From: Lean Mendoza Date: Thu, 21 Sep 2023 00:35:57 -0300 Subject: [PATCH 5/5] emotes and pointer events working --- .../packages/shared/apis/host/EngineAPI.ts | 67 +++++++++++++++++-- .../packages/shared/apis/host/context.ts | 2 + .../shared/apis/host/runtime7/avatar/ecs.ts | 62 ++++++++++++++--- .../shared/apis/host/runtime7/avatar/index.ts | 30 +++++++-- .../packages/shared/sceneEvents/sagas.ts | 3 + .../packages/shared/world/SceneWorker.ts | 2 + .../test/unit/RestrictedActions.test.tsx | 2 + 7 files changed, 151 insertions(+), 17 deletions(-) diff --git a/browser-interface/packages/shared/apis/host/EngineAPI.ts b/browser-interface/packages/shared/apis/host/EngineAPI.ts index 02f133df13..5f02e7faa6 100644 --- a/browser-interface/packages/shared/apis/host/EngineAPI.ts +++ b/browser-interface/packages/shared/apis/host/EngineAPI.ts @@ -8,8 +8,10 @@ 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 { PBPointerEventsResult, Sdk7ComponentIds } from './runtime7/avatar/ecs' import { buildAvatarTransformMessage } from './runtime7/serialization/transform' +import { AppendValueOperation } from './runtime7/serialization/crdt/appendValue' +import { InputAction, PointerEventType } from 'shared/protocol/decentraland/sdk/components/common/input_action.gen' function getParcelNumber(x: number, z: number) { return z * 100e8 + x @@ -66,6 +68,54 @@ export function registerEngineApiServiceServerImplementation(port: RpcServerPort sdk7AvatarUpdates.push(tempReusableBuffer.toCopiedBinary()) } }) + + ctx.subscribedEvents.add('playerClicked') + } + + const crdtReusableBuffer = new ReadWriteByteBuffer() + let localTimestamp = 0 + function getPlayerClickedEvents(): Uint8Array[] { + const msgs: Uint8Array[] = [] + const playerClickedEvents = ctx.events.filter((value) => value.generic?.eventId === 'playerClicked') + for (const event of playerClickedEvents) { + event.generic?.eventData + const { userId, ray } = JSON.parse(event.generic!.eventData) + const { origin, direction, distance } = ray + + localTimestamp++ + const entityId = avatarSdk7Ecs.ensureAvatarEntityId(userId) + + { + crdtReusableBuffer.resetBuffer() + + const writer = PBPointerEventsResult.encode({ + button: InputAction.IA_POINTER, + hit: { + position: origin, + globalOrigin: origin, + direction, + normalHit: undefined, + length: distance, + entityId: entityId + }, + state: PointerEventType.PET_DOWN, + timestamp: localTimestamp, + tickNumber: ctx.tickNumber + }) + const buffer = new Uint8Array(writer.finish(), 0, writer.len) + AppendValueOperation.write( + entityId, + localTimestamp, + Sdk7ComponentIds.POINTER_EVENTS_RESULT, + buffer, + crdtReusableBuffer + ) + + const messageData = crdtReusableBuffer.toCopiedBinary() + msgs.push(messageData) + } + } + return msgs } return { @@ -79,7 +129,7 @@ export function registerEngineApiServiceServerImplementation(port: RpcServerPort if (events.length) { ctx.events = [] } - + ctx.sendBatchCalled = true return { events } }, @@ -103,10 +153,19 @@ export function registerEngineApiServiceServerImplementation(port: RpcServerPort payload: req.data }) - const avatarStates = sdk7AvatarUpdates + const avatarPointerEvents = getPlayerClickedEvents() + const data: Uint8Array[] = [ret.payload, ...avatarPointerEvents, ...sdk7AvatarUpdates] + sdk7AvatarUpdates = [] + ctx.tickNumber++ + + // If the sendBatch is not being called after 10 ticks, clean the events her (after getting data from playerClickedEvents) + // Corner case: if the player click other player in this windows of 10 ticks, it'll receive a bunch of times + if (!ctx.sendBatchCalled && ctx.tickNumber > 10) { + ctx.events = [] + } - return { data: [ret.payload, ...avatarStates] } + return { data } }, // @deprecated diff --git a/browser-interface/packages/shared/apis/host/context.ts b/browser-interface/packages/shared/apis/host/context.ts index 4525262dea..b12480b977 100644 --- a/browser-interface/packages/shared/apis/host/context.ts +++ b/browser-interface/packages/shared/apis/host/context.ts @@ -21,6 +21,8 @@ export type PortContext = { subscribedEvents: Set events: EventData[] + tickNumber: number + sendBatchCalled: boolean avatarEntityInsideScene: Map // @deprecated diff --git a/browser-interface/packages/shared/apis/host/runtime7/avatar/ecs.ts b/browser-interface/packages/shared/apis/host/runtime7/avatar/ecs.ts index 30af8d253f..d93298ea6a 100644 --- a/browser-interface/packages/shared/apis/host/runtime7/avatar/ecs.ts +++ b/browser-interface/packages/shared/apis/host/runtime7/avatar/ecs.ts @@ -1,5 +1,6 @@ import { NewProfileForRenderer } from 'lib/decentraland/profiles/transformations' import { ReceiveUserExpressionMessage } from 'shared/comms/interface/types' +import { ProfileUserInfo } from 'shared/profiles/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' @@ -10,6 +11,8 @@ import { AppendValueOperation } from '../serialization/crdt/appendValue' import { DeleteEntity } from '../serialization/crdt/deleteEntity' import { PutComponentOperation } from '../serialization/crdt/putComponent' +export { PBPointerEventsResult } from '../../../../protocol/decentraland/sdk/components/pointer_events_result.gen' + const MAX_ENTITY_VERSION = 0xffff const AVATAR_RESERVED_ENTITY_NUMBER = { from: 10, to: 200 } @@ -18,7 +21,8 @@ export const Sdk7ComponentIds = { AVATAR_BASE: 1087, AVATAR_EMOTE_COMMAND: 1088, PLAYER_IDENTITY_DATA: 1089, - AVATAR_EQUIPPED_DATA: 1091 + AVATAR_EQUIPPED_DATA: 1091, + POINTER_EVENTS_RESULT: 1063 } export function createTinyEcs() { @@ -112,13 +116,10 @@ export function createTinyEcs() { return crdtReusableBuffer.toCopiedBinary() } - function updateProfile(entity: Entity, data: NewProfileForRenderer): Uint8Array[] { + function handleNewProfile(entity: Entity, data: NewProfileForRenderer, bypassEarlyExit: boolean): 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() @@ -133,9 +134,13 @@ export function createTinyEcs() { const messageData = crdtReusableBuffer.toCopiedBinary() playerIdentityComponent.set(entity, { ts: timestamp, lastMessageData: messageData }) msgs.push(messageData) + } else if (!bypassEarlyExit) { + return [] } - // Update avatar base + const avatarBaseComponent = getComponentTimestamp(Sdk7ComponentIds.AVATAR_BASE) + const avatarEquippedComponent = getComponentTimestamp(Sdk7ComponentIds.AVATAR_EQUIPPED_DATA) + { crdtReusableBuffer.resetBuffer() @@ -155,7 +160,6 @@ export function createTinyEcs() { msgs.push(messageData) } - // Update avatar equipped data { crdtReusableBuffer.resetBuffer() @@ -174,6 +178,47 @@ export function createTinyEcs() { return msgs } + function updateProfile(entity: Entity, profile: ProfileUserInfo): Uint8Array[] { + const msgs: Uint8Array[] = [] + const avatarBaseComponent = getComponentTimestamp(Sdk7ComponentIds.AVATAR_BASE) + const avatarEquippedComponent = getComponentTimestamp(Sdk7ComponentIds.AVATAR_EQUIPPED_DATA) + + { + crdtReusableBuffer.resetBuffer() + + const writer = PBAvatarBase.encode({ + skinColor: profile.data.avatar.skin.color, + eyesColor: profile.data.avatar.eyes.color, + hairColor: profile.data.avatar.hair.color, + bodyShapeUrn: profile.data.avatar.bodyShape, + name: profile.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) + } + + { + crdtReusableBuffer.resetBuffer() + + const writer = PBAvatarEquippedData.encode({ + wearableUrns: profile.data.avatar.wearables, + emotesUrns: (profile.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 @@ -224,8 +269,9 @@ export function createTinyEcs() { removeAvatarEntityId, computeNextAvatarTransformTimestamp, - updateProfile, appendAvatarEmoteCommand, + updateProfile, + handleNewProfile, getState } diff --git a/browser-interface/packages/shared/apis/host/runtime7/avatar/index.ts b/browser-interface/packages/shared/apis/host/runtime7/avatar/index.ts index c79f4af6fa..04ccfdcc8f 100644 --- a/browser-interface/packages/shared/apis/host/runtime7/avatar/index.ts +++ b/browser-interface/packages/shared/apis/host/runtime7/avatar/index.ts @@ -1,4 +1,6 @@ import mitt from 'mitt' +import { getProfileFromStore } from 'shared/profiles/selectors' +import { store } from 'shared/store/isolatedStore' import { avatarMessageObservable } from '../../../../comms/peers' import * as rfc4 from '../../../../protocol/decentraland/kernel/comms/rfc4/comms.gen' import { Entity } from '../engine/entity' @@ -37,12 +39,19 @@ avatarMessageObservable.add((evt) => { data: evt.data.position, ts: timestamp }) - } - if (evt.profile) { - const avatarBase = avatarSdk7Ecs.updateProfile(avatarEntityId, evt.profile) - for (const msg of avatarBase) { - avatarSdk7MessageObservable.emit('BinaryMessage', msg) + if (evt.profile) { + const avatarBase = avatarSdk7Ecs.handleNewProfile(avatarEntityId, evt.profile, false) + for (const msg of avatarBase) { + avatarSdk7MessageObservable.emit('BinaryMessage', msg) + } + } + } else { + if (evt.profile) { + const avatarBase = avatarSdk7Ecs.handleNewProfile(avatarEntityId, evt.profile, true) + for (const msg of avatarBase) { + avatarSdk7MessageObservable.emit('BinaryMessage', msg) + } } } } else if (evt.type === 'USER_EXPRESSION') { @@ -53,3 +62,14 @@ avatarMessageObservable.add((evt) => { avatarSdk7MessageObservable.emit('RemoveAvatar', { entity: avatarEntityId, data: avatarRemoveEntity }) } }) + +export function* avatarSdk7ProfileChanged(userId: string) { + const profile = getProfileFromStore(store.getState(), userId) + + if (!profile?.data) { + return {} + } + + const avatarEntityId = avatarSdk7Ecs.ensureAvatarEntityId(userId) + avatarSdk7Ecs.updateProfile(avatarEntityId, profile) +} diff --git a/browser-interface/packages/shared/sceneEvents/sagas.ts b/browser-interface/packages/shared/sceneEvents/sagas.ts index ebd0e16983..e91db66dea 100644 --- a/browser-interface/packages/shared/sceneEvents/sagas.ts +++ b/browser-interface/packages/shared/sceneEvents/sagas.ts @@ -14,6 +14,7 @@ import { getCommsIsland } from '../comms/selectors' import { SAVE_DELTA_PROFILE_REQUEST } from '../profiles/actions' import { takeLatestByUserId } from '../profiles/sagas' import { allScenesEvent } from '../world/parcelSceneManager' +import { avatarSdk7ProfileChanged } from 'shared/apis/host/runtime7/avatar' export function* sceneEventsSaga() { yield takeLatest([SET_COMMS_ISLAND, SET_ROOM_CONNECTION, SET_REALM_ADAPTER], islandChanged) @@ -63,6 +64,8 @@ function* submitProfileToScenes() { version: profile.version } }) + + yield call(avatarSdk7ProfileChanged, profile.ethAddress) } } diff --git a/browser-interface/packages/shared/world/SceneWorker.ts b/browser-interface/packages/shared/world/SceneWorker.ts index c9d4cdb5ff..f3c3ec7adc 100644 --- a/browser-interface/packages/shared/world/SceneWorker.ts +++ b/browser-interface/packages/shared/world/SceneWorker.ts @@ -166,6 +166,8 @@ export class SceneWorker { scenePort, rpcSceneControllerService, avatarEntityInsideScene: new Map(), + tickNumber: 0, + sendBatchCalled: false, sceneData: { isPortableExperience: false, useFPSThrottling: false, diff --git a/browser-interface/test/unit/RestrictedActions.test.tsx b/browser-interface/test/unit/RestrictedActions.test.tsx index 8934e8a36e..7d9852598e 100644 --- a/browser-interface/test/unit/RestrictedActions.test.tsx +++ b/browser-interface/test/unit/RestrictedActions.test.tsx @@ -140,6 +140,8 @@ describe('RestrictedActions tests', () => { subscribedEvents: new Set(), events: [], avatarEntityInsideScene: new Map(), + sendBatchCalled: false, + tickNumber: 0, sendProtoSceneEvent() { throw new Error('not implemented') },