diff --git a/docs/protocol/avatar-schema.md b/docs/protocol/avatar-schema.md new file mode 100644 index 000000000..5c45f0394 --- /dev/null +++ b/docs/protocol/avatar-schema.md @@ -0,0 +1,60 @@ +# Avatar Network Schema + +```c +struct Spawn { + networkId: uint64 // The avatar's networkId + authorIndex: uint64 // Who controls the avatar + schemaId: uint32 // 1 (Reserved Avatar schema id) + creationDataByteLength: uint32 // 0 + creationData: uint8[] // not used + updateData: AvatarUpdate +} + +struct AvatarUpdate { + changedBitmask: uint8_t // rigPosition, rigRotation, rigVelocity + rigPosition: float_t[3] + rigRotation: float_t[4] + rigVelocity: float_t[3] +} + +struct AvatarXRModeUpdate { + xrMode: uint8_t +} +``` + +```js +const avatarReplicator = createNetworkedReplicator({ + spawn(world, { author, creationData, updateData, networked }) { + // Create node in world + // Should be controllable by author + // Return node to spawn + return avatarRoot; + }, + encode(node, buffer) { + // Write to buffer, return number bytes written, buffer truncated to that length and written as updateData + }, + decode(node, buffer) { + // Read from buffer and apply to node or interp buffer + const interpBuffer = interpMap.get(node); + interpBuffer.add(); + }, +}); + +// host +if (host) { + const node = avatarReplicator.spawn(creationData, { destroyOnLeave: true }); + // Modify the node as needed and that update data will be sent with the first spawn message +} + +avatarReplicator.despawn(node); + +for (const { node } of avatarReplicator.spawned()) { + // Spawned nodes should have the latest update data applied to them if it exists, + // not just the original spawn update data + world.environment.addNode(node); +} + +for (const node of avatarReplicator.despawned()) { + world.environment.removeNode(node); +} +``` diff --git a/docs/protocol/implementation.md b/docs/protocol/implementation.md index a69299954..231160394 100644 --- a/docs/protocol/implementation.md +++ b/docs/protocol/implementation.md @@ -22,11 +22,12 @@ Three components are used to determine how the peer's simulation treats each net - Enter Query [Networked, Authoring] - if we are the current host (if Networked.peerIndex is our peerIndex), we can safely send out a spawn across the network for this entity - if we are a new host being migrated to, the entity has spawned on the network already, therefore do not execute a spawn for this entity + - if we are a new host first joining, wait for HostSnapshot - Exit Query [Networked, Authoring] - if we are the current host (if Networked.peerIndex is our peerIndex), we can safely send out a despawn across the network for this entity - if we are a old host being migrated away from, we should not send a despawn across the network for this entity - Relaying - - This component indicates that this peer is hosting the simulation for this entity, but is only responsible for relaying the source-of-truth state for this entity which is being received by another peer who is authoring the source-of-truth for the entity + - This component indicates that the current peer is only responsible for relaying the source-of-truth which is being received by the authoring peer. - Holds the peerIndex of the peer who we are relaying the source-of-truth for ### Networked Object Lifecycle diff --git a/docs/protocol/index.md b/docs/protocol/index.md index 721ded174..1007bbd56 100644 --- a/docs/protocol/index.md +++ b/docs/protocol/index.md @@ -8,7 +8,7 @@ Client uses [host election algorithm](#host-election) to determine host among me Client waits to connect to host's `WebRTCPeerConnection` and ensures that the `RTCDataChannel` is open. -Wait for a `HostSnapshot` message from the host. Note you may receive messages from clients that are not the determined host. This could either be malicious or the result of the Matrix Room's state events not being up to date yet. If the message is from the host, set the local peer index to the `localPeerIndex` returned in the `HostSnapshot` message and the host peer index to the `hostPeerIndex`. If it is not from the host, store the message in case the current host changes. If the connection timeout happens before a `HostSnapshot` message is received stop waiting and show an error to the user and a button allowing them to try to reconnect. +If our client is not determined as host, wait for a `HostSnapshot` message from the host. Note you may receive messages from clients that are not the determined host. This could either be malicious or the result of the Matrix Room's state events not being up to date yet. If the message is from the host, set the local peer index to the `localPeerIndex` returned in the `HostSnapshot` message and the host peer index to the `hostPeerIndex`. If it is not from the host, store the message in case the current host changes. If the connection timeout happens before a `HostSnapshot` message is received stop waiting and show an error to the user and a button allowing them to try to reconnect. ### Disconnect @@ -81,6 +81,7 @@ struct PeerInfo { struct Spawn { networkId: uint64 + authorIndex: uint64 schemaId: uint32 creationDataByteLength: uint32 creationData: uint8[] @@ -93,7 +94,6 @@ struct Despawn { struct Update { networkId: uint64 // The network id of the spawned node - bitmask: uint8/16/32 // a bitmask indicating which properties of the schema are included in this update data: uint8[] // The update data } @@ -118,11 +118,11 @@ message HostSnapshot { hostTime: uint64 localPeerIndex: uint64 hostPeerIndex: uint64 - peerCount: uint32 - peers: PeerInfo[peerCount] - entityCount: uint32 + entityCount: uint16 entitySpawns: Spawn[entityCount] - hostStateByteLength: uint32 + peerCount: uint16 + peers: PeerInfo[peerCount] + hostStateByteLength: uint16 hostState: uint8[char] } diff --git a/src/engine/allocator/CursorView.ts b/src/engine/allocator/CursorView.ts index 409dbc757..3b4459f18 100644 --- a/src/engine/allocator/CursorView.ts +++ b/src/engine/allocator/CursorView.ts @@ -130,14 +130,14 @@ export const writeFloat32 = (v: CursorView, value: number) => { return v; }; -export const writeUint64 = (v: CursorView, value: number) => { - v.setUint64(v.cursor, value, v.littleEndian); +export const writeUint64 = (v: CursorView, value: bigint) => { + v.setBigInt64(v.cursor, value, v.littleEndian); v.cursor += BigUint64Array.BYTES_PER_ELEMENT; return v; }; -export const writeInt64 = (v: CursorView, value: number) => { - v.setInt64(v.cursor, value, v.littleEndian); +export const writeInt64 = (v: CursorView, value: bigint) => { + v.setBigInt64(v.cursor, value, v.littleEndian); v.cursor += BigInt64Array.BYTES_PER_ELEMENT; return v; }; @@ -209,8 +209,8 @@ export const spaceFloat32 = (v: CursorView) => { export const spaceUint64 = (v: CursorView) => { const savePoint = v.cursor; v.cursor += BigUint64Array.BYTES_PER_ELEMENT; - return (value: number) => { - v.setUint64(savePoint, value, v.littleEndian); + return (value: bigint) => { + v.setBigUint64(savePoint, value, v.littleEndian); return v; }; }; @@ -218,8 +218,8 @@ export const spaceUint64 = (v: CursorView) => { export const spaceInt64 = (v: CursorView) => { const savePoint = v.cursor; v.cursor += BigInt64Array.BYTES_PER_ELEMENT; - return (value: number) => { - v.setInt64(savePoint, value, v.littleEndian); + return (value: bigint) => { + v.setBigInt64(savePoint, value, v.littleEndian); return v; }; }; diff --git a/src/engine/config.game.ts b/src/engine/config.game.ts index 438841efb..80e6ba736 100644 --- a/src/engine/config.game.ts +++ b/src/engine/config.game.ts @@ -3,7 +3,7 @@ import { AudioModule } from "./audio/audio.game"; import { InputModule } from "./input/input.game"; import { UpdateRawInputSystem, ResetRawInputSystem } from "./input/RawInputSystems"; import { PhysicsModule, PhysicsSystem } from "./physics/physics.game"; -import { NetworkExitWorldQueueSystem, NetworkModule } from "./network/network.game"; +import { NetworkThreadedMessageQueueSystem, NetworkModule, HostSpawnPeerAvatarSystem } from "./network/network.game"; import { ActionMappingSystem } from "./input/ActionMappingSystem"; import { KinematicCharacterControllerModule, @@ -17,7 +17,7 @@ import { } from "./editor/editor.game"; import { GameContext } from "./GameTypes"; import { RendererModule } from "./renderer/renderer.game"; -import { SpawnablesModule } from "../plugins/spawnables/spawnables.game"; +import { SpawnablesModule, SpawnablesSystem } from "../plugins/spawnables/spawnables.game"; import { RecycleResourcesSystem, ResourceDisposalSystem, @@ -28,7 +28,7 @@ import { import { ThirdRoomModule, WorldLoaderSystem } from "../plugins/thirdroom/thirdroom.game"; import { UpdateMatrixWorldSystem } from "./component/transform"; import { FlyCharacterControllerModule, FlyControllerSystem } from "./player/FlyCharacterController"; -import { NetworkInterpolationSystem } from "./network/NetworkInterpolationSystem"; +// import { NetworkInterpolationSystem } from "./network/NetworkInterpolationSystem"; import { PrefabDisposalSystem, PrefabModule } from "./prefab/prefab.game"; import { AnimationSystem } from "./animation/animation.game"; import { @@ -40,8 +40,8 @@ import { NametagModule, NametagSystem } from "./player/nametags.game"; import { ScriptingSystem } from "./scripting/scripting.game"; import { GameResourceSystem } from "./resource/GameResourceSystem"; import { RemoteCameraSystem } from "./camera/camera.game"; -import { InboundNetworkSystem } from "./network/inbound.game"; -import { OutboundNetworkSystem } from "./network/outbound.game"; +import { InboundNetworkSystem } from "./network/InboundNetworkSystem"; +import { OutboundNetworkSystem } from "./network/OutboundNetworkSystem"; import { GLTFResourceDisposalSystem } from "./gltf/gltf.game"; import { IncomingTripleBufferSystem } from "./resource/IncomingTripleBufferSystem"; import { OutgoingTripleBufferSystem } from "./resource/OutgoingTripleBufferSystem"; @@ -55,6 +55,8 @@ import { PlayerModule } from "./player/Player.game"; import { ActionBarSystem } from "../plugins/thirdroom/action-bar.game"; import { EnableCharacterControllerSystem } from "./player/CharacterController"; import { CameraRigSystem } from "./player/CameraRig"; +// import { TransferAuthoritySystem } from "./network/TransferAuthoritySystem"; +import { DespawnAvatarSystem, SpawnAvatarSystem } from "./player/PlayerRig"; export default defineConfig({ modules: [ @@ -87,6 +89,10 @@ export default defineConfig({ ActionMappingSystem, InboundNetworkSystem, + // TransferAuthoritySystem, + HostSpawnPeerAvatarSystem, + SpawnAvatarSystem, + DespawnAvatarSystem, WorldLoaderSystem, @@ -98,13 +104,15 @@ export default defineConfig({ InteractionSystem, XRInteractionSystem, ActionBarSystem, + SpawnablesSystem, EnableCharacterControllerSystem, - // step physics forward and copy rigidbody data to transform component + // step physics forward and sync physics bodies with node transforms PhysicsSystem, // interpolate towards authoritative state - NetworkInterpolationSystem, + // TODO: rewrite + // NetworkInterpolationSystem, ScriptingSystem, @@ -117,7 +125,7 @@ export default defineConfig({ //EditorSelectionSystem, OutboundNetworkSystem, - NetworkExitWorldQueueSystem, + NetworkThreadedMessageQueueSystem, RemoteCameraSystem, PrefabDisposalSystem, diff --git a/src/engine/editor/editor.game.ts b/src/engine/editor/editor.game.ts index cb6c4c6cc..517f46029 100644 --- a/src/engine/editor/editor.game.ts +++ b/src/engine/editor/editor.game.ts @@ -46,7 +46,7 @@ import { disableActionMap, enableActionMap } from "../input/ActionMappingSystem" import { ActionMap, ActionType, BindingType, ButtonActionState } from "../input/ActionMap"; import { InputModule } from "../input/input.game"; import { flyControlsQuery } from "../player/FlyCharacterController"; -import { getCamera } from "../player/getCamera"; +import { tryGetCamera } from "../player/getCamera"; import { setRenderNodeOptimizationsEnabled } from "../renderer/renderer.game"; /********* @@ -304,7 +304,7 @@ export function EditorStateSystem(ctx: GameContext) { for (let i = 0; i < ents.length; i++) { const playerRigEid = ents[i]; const playerRig = tryGetRemoteResource(ctx, playerRigEid); - getCamera(ctx, playerRig); + tryGetCamera(ctx, playerRig); const body = playerRig.physicsBody?.body; diff --git a/src/engine/input/WebXRAvatarRigSystem.ts b/src/engine/input/WebXRAvatarRigSystem.ts index b3aadd972..1b48f33c1 100644 --- a/src/engine/input/WebXRAvatarRigSystem.ts +++ b/src/engine/input/WebXRAvatarRigSystem.ts @@ -1,4 +1,4 @@ -import { addComponent, defineQuery, exitQuery, hasComponent, Not, removeComponent } from "bitecs"; +import { addComponent, defineQuery, enterQuery, exitQuery, hasComponent, Not, removeComponent } from "bitecs"; import { mat4, quat, vec3 } from "gl-matrix"; import { FlyControls } from "../player/FlyCharacterController"; @@ -22,13 +22,13 @@ import { getRemoteResource, tryGetRemoteResource } from "../resource/resource.ga import { teleportEntity } from "../utils/teleportEntity"; import { ActionMap, ActionType, BindingType, ButtonActionState } from "./ActionMap"; import { InputModule } from "./input.game"; -import { Networked, Owned } from "../network/NetworkComponents"; -import { broadcastReliable } from "../network/outbound.game"; +import { Networked, Authoring } from "../network/NetworkComponents"; import { createInformXRModeMessage } from "../network/serialization.game"; import { NetworkModule } from "../network/network.game"; -import { XRHeadComponent, XRControllerComponent } from "../player/PlayerRig"; import { AvatarRef } from "../player/components"; import { ourPlayerQuery } from "../player/Player"; +import { XRControllerComponent, XRHeadComponent } from "../player/XRComponents"; +import { enqueueReliableBroadcast } from "../network/NetworkRingBuffer"; export interface XRAvatarRig { prevLeftAssetPath?: string; @@ -60,9 +60,14 @@ export function addXRAvatarRig(world: World, eid: number) { const xrAvatarRigQuery = defineQuery([XRAvatarRig]); const xrAvatarRigExitQuery = exitQuery(xrAvatarRigQuery); -const remoteXRControllerQuery = defineQuery([Networked, Not(Owned), XRControllerComponent]); -const remoteXRHeadQuery = defineQuery([Networked, Not(Owned), XRHeadComponent]); -const remoteAvatarQuery = defineQuery([Networked, Not(Owned), AvatarRef]); +const remoteXRControllerQuery = defineQuery([Networked, Not(Authoring), XRControllerComponent]); +const enteredRemoteXRControllerQuery = enterQuery(remoteXRControllerQuery); + +const remoteXRHeadQuery = defineQuery([Networked, Not(Authoring), XRHeadComponent]); +const enteredRemoteXRHeadQuery = enterQuery(remoteXRHeadQuery); + +const remoteAvatarQuery = defineQuery([Networked, Not(Authoring), AvatarRef]); +const enteredRemoteAvatarQuery = enterQuery(remoteAvatarQuery); const _v = vec3.create(); const _q = quat.create(); @@ -107,7 +112,7 @@ export function WebXRAvatarRigSystem(ctx: GameContext) { rendererModule.prevXRMode = ourXRMode; // inform other clients of our XRMode - broadcastReliable(ctx, network, createInformXRModeMessage(ctx, ourXRMode)); + enqueueReliableBroadcast(network, createInformXRModeMessage(ctx, ourXRMode)); } for (let i = 0; i < rigs.length; i++) { @@ -173,40 +178,41 @@ export function WebXRAvatarRigSystem(ctx: GameContext) { // determine visibility of XR objects depending on XR modes of the clients who own those objects - const remoteXRControllers = remoteXRControllerQuery(ctx.world); + const remoteXRControllers = enteredRemoteXRControllerQuery(ctx.world); for (let i = 0; i < remoteXRControllers.length; i++) { const eid = remoteXRControllers[i]; const node = tryGetRemoteResource(ctx, eid); - const peerId = network.entityIdToPeerId.get(eid)!; - const theirXRMode = network.peerIdToXRMode.get(peerId)!; + const peer = network.entityIdToPeer.get(eid)!; + const theirXRMode = peer.xrMode; // hands are hidden for AR participants node.visible = !(sceneSupportsAR && ourXRMode === XRMode.ImmersiveAR && theirXRMode === XRMode.ImmersiveAR); } - const remoteXRHeads = remoteXRHeadQuery(ctx.world); + const remoteXRHeads = enteredRemoteXRHeadQuery(ctx.world); for (let i = 0; i < remoteXRHeads.length; i++) { const eid = remoteXRHeads[i]; const node = tryGetRemoteResource(ctx, eid); - const peerId = network.entityIdToPeerId.get(eid)!; - const theirXRMode = network.peerIdToXRMode.get(peerId)!; + const peer = network.entityIdToPeer.get(eid)!; + const theirXRMode = peer.xrMode; // heads are hidden for AR participants node.visible = !(sceneSupportsAR && ourXRMode === XRMode.ImmersiveAR && theirXRMode === XRMode.ImmersiveAR); } - const remoteAvatars = remoteAvatarQuery(ctx.world); + const remoteAvatars = enteredRemoteAvatarQuery(ctx.world); for (let i = 0; i < remoteAvatars.length; i++) { const eid = remoteAvatars[i]; const node = tryGetRemoteResource(ctx, eid); - const peerId = network.entityIdToPeerId.get(eid)!; - const theirXRMode = network.peerIdToXRMode.get(peerId)!; + // const peerId = network.entityIdToPeerId.get(eid)!; + // const theirXRMode = network.peerIdToXRMode.get(peerId)!; const avatarEid = AvatarRef.eid[node.eid]; const avatar = tryGetRemoteResource(ctx, avatarEid); // regular avatar is hidden for XR participants - avatar.visible = !(theirXRMode === XRMode.ImmersiveAR || theirXRMode === XRMode.ImmersiveVR); + // TODO + // avatar.visible = !(theirXRMode === XRMode.ImmersiveAR || theirXRMode === XRMode.ImmersiveVR); // re-scale if the remote avatar is visible and we are AR if (avatar.visible && ourXRMode === XRMode.ImmersiveAR) { @@ -322,7 +328,7 @@ function updateXRController( // networked ray const networkedRayNode = createPrefabEntity(ctx, "xr-ray"); - addComponent(ctx.world, Owned, networkedRayNode.eid); + addComponent(ctx.world, Authoring, networkedRayNode.eid); addComponent(ctx.world, Networked, networkedRayNode.eid); addObjectToWorld(ctx, networkedRayNode); @@ -330,7 +336,7 @@ function updateXRController( // networked hand const networkedController = createPrefabEntity(ctx, `xr-hand-${hand}`); - addComponent(ctx.world, Owned, networkedController.eid); + addComponent(ctx.world, Authoring, networkedController.eid); addComponent(ctx.world, Networked, networkedController.eid); networkedController.visible = false; @@ -404,7 +410,7 @@ function updateXRCamera( if (!cameraNode || !xrRig.cameraEid) { cameraNode = createPrefabEntity(ctx, "xr-head"); - addComponent(ctx.world, Owned, cameraNode.eid); + addComponent(ctx.world, Authoring, cameraNode.eid); addComponent(ctx.world, Networked, cameraNode.eid); cameraNode.visible = false; addObjectToWorld(ctx, cameraNode); diff --git a/src/engine/input/input.game.ts b/src/engine/input/input.game.ts index 853f33dcc..36717d5ef 100644 --- a/src/engine/input/input.game.ts +++ b/src/engine/input/input.game.ts @@ -1,7 +1,7 @@ import { ourPlayerQuery } from "../player/Player"; import { GameContext } from "../GameTypes"; import { defineModule, getModule, Thread } from "../module/module.common"; -import { getCamera } from "../player/getCamera"; +import { tryGetCamera } from "../player/getCamera"; import { XRMode } from "../renderer/renderer.common"; import { getXRMode } from "../renderer/renderer.game"; import { RemoteNode } from "../resource/RemoteResources"; @@ -58,6 +58,6 @@ export function getPrimaryInputSourceNode(ctx: GameContext) { return rightRayNode; } else { const playerNode = getRemoteResource(ctx, ourPlayer) as RemoteNode; - return getCamera(ctx, playerNode).parent as RemoteNode; + return tryGetCamera(ctx, playerNode).parent as RemoteNode; } } diff --git a/src/engine/network/Codec.test.ts b/src/engine/network/Codec.test.ts new file mode 100644 index 000000000..4f19faabf --- /dev/null +++ b/src/engine/network/Codec.test.ts @@ -0,0 +1,186 @@ +import { assert } from "vitest"; + +import { createCursorView, moveCursorView, readFloat32, readUint8 } from "../allocator/CursorView"; +import { Binary, createAutoDecoder, createAutoEncoder, createMutationDecoder, createMutationEncoder } from "./Codec"; + +describe("Codec Tests", () => { + describe("Auto Encoder/Decoder", () => { + test("should encode", () => { + const view = createCursorView(new ArrayBuffer(1)); + + const schema = { data: Binary.ui8 }; + + const encode = createAutoEncoder(schema); + encode(view, { data: 1 }); + + moveCursorView(view, 0); + assert.strictEqual(readUint8(view), 1); + }); + test("should decode", () => { + const view = createCursorView(new ArrayBuffer(1)); + + const schema = { data: Binary.ui8 }; + + const encode = createAutoEncoder(schema); + const decode = createAutoDecoder(schema); + + encode(view, { data: 1 }); + + moveCursorView(view, 0); + const obj = decode(view, { data: 0 }); + + assert.isDefined(obj); + assert.strictEqual(obj.data, 1); + }); + test("should work with arrays", () => { + const view = createCursorView(new ArrayBuffer(3)); + + const schema = { [0]: Binary.ui8, [1]: Binary.ui8, [2]: Binary.ui8 }; + + const encode = createAutoEncoder(schema); + const decode = createAutoDecoder(schema); + + encode(view, [1, 1, 1]); + + moveCursorView(view, 0); + const obj = decode(view, [0, 0, 0])!; + + assert.isDefined(obj); + assert.strictEqual(obj[0], 1); + }); + }); + + describe("Mutation Encoder/Decoder", () => { + test("should encode mutations only", () => { + const view = createCursorView(new ArrayBuffer(7)); + + const schema = { a: Binary.ui8, b: Binary.f32 }; + + const encode = createMutationEncoder(schema); + const obj = { a: 1, b: 2.5 }; + encode(view, obj); + + moveCursorView(view, 0); + assert.strictEqual(readUint8(view), 0b11); + assert.strictEqual(readUint8(view), 1); + assert.strictEqual(readFloat32(view), 2.5); + + moveCursorView(view, 0); + encode(view, obj); + + moveCursorView(view, 0); + assert.strictEqual(readUint8(view), 0b0); + + obj.a = 2; + + moveCursorView(view, 0); + encode(view, obj); + + moveCursorView(view, 0); + assert.strictEqual(readUint8(view), 0b1); + assert.strictEqual(readUint8(view), 2); + + obj.b = 3.5; + + moveCursorView(view, 0); + encode(view, obj); + + moveCursorView(view, 0); + assert.strictEqual(readUint8(view), 0b10); + assert.strictEqual(readFloat32(view), 3.5); + + obj.a = 0; + obj.b = 0; + + moveCursorView(view, 0); + encode(view, obj); + + moveCursorView(view, 0); + assert.strictEqual(readUint8(view), 0b11); + assert.strictEqual(readUint8(view), 0); + assert.strictEqual(readFloat32(view), 0); + }); + test("should decode mutations only", () => { + const view = createCursorView(new ArrayBuffer(7)); + + const schema = { a: Binary.ui8, b: Binary.f32 }; + + const encode = createMutationEncoder(schema); + const decode = createMutationDecoder(schema); + + const objSource = { a: 1, b: 2.5 }; + const objSink = { a: 0, b: 0 }; + + encode(view, objSource); + + moveCursorView(view, 0); + decode(view, objSink); + + assert.strictEqual(objSink.a, 1); + assert.strictEqual(objSink.b, 2.5); + + moveCursorView(view, 0); + encode(view, objSource); + + objSink.a = 0; + + moveCursorView(view, 0); + decode(view, objSink); + + assert.strictEqual(objSink.a, 0); + assert.strictEqual(objSink.b, 2.5); + + objSource.a = 2; + + moveCursorView(view, 0); + encode(view, objSource); + assert.strictEqual(objSource.a, 2); + assert.strictEqual(objSink.b, 2.5); + + objSource.b = 3.5; + + moveCursorView(view, 0); + encode(view, objSource); + assert.strictEqual(objSource.a, 2); + assert.strictEqual(objSource.b, 3.5); + + objSource.a = 0; + objSource.b = 0; + + moveCursorView(view, 0); + encode(view, objSource); + assert.strictEqual(objSource.a, 0); + assert.strictEqual(objSource.b, 0); + }); + test("should work with array mutations", () => { + const view = createCursorView(new ArrayBuffer(4)); + + // const schema = { [0]: Binary.ui8, [1]: Binary.ui8, [2]: Binary.ui8 }; + const schema = [Binary.ui8, Binary.ui8, Binary.ui8]; + + const encode = createMutationEncoder(schema); + const decode = createMutationDecoder(schema); + + const arrSource = [0, 1, 0]; + const arrSink = [0, 0, 0]; + + encode(view, arrSource); + + moveCursorView(view, 0); + decode(view, arrSink); + + assert.deepEqual(arrSink, [0, 1, 0]); + + arrSource[0] = 2; + arrSource[2] = 3; + + moveCursorView(view, 0); + encode(view, arrSource); + + moveCursorView(view, 0); + decode(view, arrSink); + + assert.deepEqual(arrSink, [2, 1, 3]); + }); + }); +}); diff --git a/src/engine/network/Codec.ts b/src/engine/network/Codec.ts new file mode 100644 index 000000000..29dcb32a5 --- /dev/null +++ b/src/engine/network/Codec.ts @@ -0,0 +1,217 @@ +import { + CursorView, + readFloat32, + readFloat64, + readInt16, + readInt32, + readInt64, + readInt8, + readUint16, + readUint32, + readUint64, + readUint8, + spaceUint16, + spaceUint32, + spaceUint8, + writeFloat32, + writeFloat64, + writeInt16, + writeInt32, + writeInt64, + writeInt8, + writeUint16, + writeUint32, + writeUint64, + writeUint8, +} from "../allocator/CursorView"; +import { checkBitflag } from "../utils/checkBitflag"; + +/** + * Types + */ + +export interface Codec { + encode: (view: CursorView, value: T) => number; + decode: (view: CursorView, value: T) => void; +} + +type ui8 = "ui8"; +type i8 = "i8"; +type ui16 = "ui16"; +type i16 = "i16"; +type ui32 = "ui32"; +type i32 = "i32"; +type f32 = "f32"; +type f64 = "f64"; +type i64 = "i64"; +type ui64 = "ui64"; + +export type BinaryType = ui8 | i8 | ui16 | i16 | ui32 | i32 | f32 | f64 | i64 | ui64; + +export const Binary = { + ui8: "ui8" as ui8, + i8: "i8" as i8, + ui16: "ui16" as ui16, + i16: "i16" as i16, + ui32: "ui32" as ui32, + i32: "i32" as i32, + f32: "f32" as f32, + f64: "f64" as f64, + i64: "i64" as i64, + ui64: "ui64" as ui64, +}; + +const BinaryToWriteFunction = { + ui8: writeUint8, + i8: writeInt8, + ui16: writeUint16, + i16: writeInt16, + ui32: writeUint32, + i32: writeInt32, + ui64: writeUint64, + i64: writeInt64, + f32: writeFloat32, + f64: writeFloat64, +}; +const BinaryToReadFunction = { + ui8: readUint8, + i8: readInt8, + ui16: readUint16, + i16: readInt16, + ui32: readUint32, + i32: readInt32, + ui64: readUint64, + i64: readInt64, + f32: readFloat32, + f64: readFloat64, +}; + +export type CodecSchema = + | BinaryType[] + | { + [key: string | number]: BinaryType; + }; + +export type ObjectType = { + [key in keyof S]: S[key] extends ui64 ? bigint : S[key] extends i64 ? bigint : number; +}; + +type AutoEncoder = (v: CursorView, object: ObjectType) => CursorView | boolean; +type AutoDecoder = (v: CursorView, object?: ObjectType) => ObjectType; + +type MutationEncoder = AutoEncoder; +type MutationDecoder = AutoDecoder; + +/** + * API + */ + +export const createAutoEncoder = + (schema: S): AutoEncoder => + (v: CursorView, object: ObjectType) => { + for (const key in schema) { + const type = schema[key]; + const value = object[key]; + const write = BinaryToWriteFunction[type]; + write(v, value as never); + } + return v; + }; + +export const createAutoDecoder = (schema: S): AutoDecoder => { + let o = {} as ObjectType; + return (v: CursorView, out?: ObjectType): ObjectType => { + if (out) o = out; + + for (const key in schema) { + const type = schema[key]; + const read = BinaryToReadFunction[type]; + o[key] = read(v) as never; + } + + return o; + }; +}; + +export const createMutationEncoder = (schema: S): MutationEncoder => { + const memoir = new Map, ObjectType>(); + + const propCount = Object.keys(schema).length; + if (propCount > 32) { + throw new Error("Mutation encoding only supports schemas with <= 32 properties"); + } + + const changeMaskSpacer = propCount <= 8 ? spaceUint8 : propCount <= 16 ? spaceUint16 : spaceUint32; + + return (v: CursorView, object: ObjectType) => { + let memo = memoir.get(object); + if (!memo) { + memo = {} as ObjectType; + memoir.set(object, memo); + } + + let mask = 0; + let bit = 0; + + const writeChangeMask = changeMaskSpacer(v); + + for (const key in schema) { + const type = schema[key]; + const lastValue = (memo as ObjectType)[key]; + const value = object[key]; + + if (lastValue !== value) { + const write = BinaryToWriteFunction[type]; + write(v, value as never); + mask |= 1 << bit++; + } else { + bit++; + } + + (memo as ObjectType)[key] = value; + } + + writeChangeMask(mask); + + return mask > 0; + }; +}; + +export const createMutationDecoder = (schema: S): MutationDecoder => { + let o = {} as ObjectType; + + const propCount = Object.keys(schema).length; + if (propCount > 32) { + throw new Error("Mutation decoding only supports schemas with <= 32 properties"); + } + + const readChangeMask = propCount <= 8 ? readUint8 : propCount <= 16 ? readUint16 : readUint32; + + return (v: CursorView, out?: ObjectType) => { + if (out) o = out; + + const mask = readChangeMask(v); + let b = 0; + + for (const key in schema) { + if (!checkBitflag(mask, 1 << b++)) { + continue; + } + const type = schema[key]; + const read = BinaryToReadFunction[type]; + o[key] = read(v) as never; + } + + return o; + }; +}; + +export const createAutoCodec = (schema: CodecSchema) => ({ + encode: createAutoEncoder(schema), + decode: createAutoDecoder(schema), +}); + +export const createMutationCodec = (schema: CodecSchema) => ({ + encode: createMutationEncoder(schema), + decode: createMutationDecoder(schema), +}); diff --git a/src/engine/network/inbound.game.ts b/src/engine/network/InboundNetworkSystem.ts similarity index 58% rename from src/engine/network/inbound.game.ts rename to src/engine/network/InboundNetworkSystem.ts index 280a3e838..3f21fe230 100644 --- a/src/engine/network/inbound.game.ts +++ b/src/engine/network/InboundNetworkSystem.ts @@ -3,27 +3,23 @@ import { availableRead } from "@thirdroom/ringbuffer"; import { createCursorView, CursorView } from "../allocator/CursorView"; import { GameContext } from "../GameTypes"; import { getModule } from "../module/module.common"; -import { GameNetworkState, NetworkModule, ownedPlayerQuery } from "./network.game"; -import { NetworkAction } from "./NetworkAction"; -import { dequeueNetworkRingBuffer } from "./RingBuffer"; -import { readMetadata } from "./serialization.game"; +import { GameNetworkState, NetworkModule } from "./network.game"; +import { NetworkMessage } from "./NetworkMessage"; +import { dequeueNetworkRingBuffer } from "./NetworkRingBuffer"; +import { readMessageType } from "./serialization.game"; const processNetworkMessage = (ctx: GameContext, peerId: string, msg: ArrayBuffer) => { - const network = getModule(ctx, NetworkModule); - const cursorView = createCursorView(msg); + const messageType = readMessageType(cursorView); - const { type: messageType, elapsed, inputTick } = readMetadata(cursorView); - - const historian = network.peerIdToHistorian.get(peerId); - - if (historian) { - // this value is written onto outgoing packet headers - historian.latestTick = inputTick; - historian.latestTime = elapsed; - historian.localTime = elapsed; - historian.needsUpdate = true; - } + // TODO: refactor network interpolator + // const network = getModule(ctx, NetworkModule); + // const historian = network.peerIdToHistorian.get(peerId); + // if (historian) { + // historian.latestTime = elapsed; + // historian.localTime = elapsed; + // historian.needsUpdate = true; + // } const { messageHandlers } = getModule(ctx, NetworkModule); @@ -31,7 +27,7 @@ const processNetworkMessage = (ctx: GameContext, peerId: string, msg: ArrayBuffe if (!handler) { console.error( "could not process network message, no handler registered for messageType", - NetworkAction[messageType] + NetworkMessage[messageType] ); return; } @@ -39,13 +35,13 @@ const processNetworkMessage = (ctx: GameContext, peerId: string, msg: ArrayBuffe handler(ctx, cursorView, peerId); }; -const ringOut = { packet: new ArrayBuffer(0), peerId: "", broadcast: false }; +const ringOut = { packet: new ArrayBuffer(0), peerKey: "", broadcast: false }; const processNetworkMessages = (ctx: GameContext, network: GameNetworkState) => { try { while (availableRead(network.incomingReliableRingBuffer)) { dequeueNetworkRingBuffer(network.incomingReliableRingBuffer, ringOut); - const { peerId, packet } = ringOut; - if (!peerId) { + const { peerKey, packet } = ringOut; + if (!peerKey) { console.error("unable to process reliable network message, peerId undefined"); continue; } @@ -54,14 +50,14 @@ const processNetworkMessages = (ctx: GameContext, network: GameNetworkState) => continue; } - processNetworkMessage(ctx, ringOut.peerId, ringOut.packet); + processNetworkMessage(ctx, ringOut.peerKey, ringOut.packet); } while (availableRead(network.incomingUnreliableRingBuffer)) { dequeueNetworkRingBuffer(network.incomingUnreliableRingBuffer, ringOut); - const { peerId, packet } = ringOut; - if (!peerId) { + const { peerKey, packet } = ringOut; + if (!peerKey) { console.error("unable to process unreliable network message, peerId undefined"); continue; } @@ -70,7 +66,7 @@ const processNetworkMessages = (ctx: GameContext, network: GameNetworkState) => continue; } - processNetworkMessage(ctx, ringOut.peerId, ringOut.packet); + processNetworkMessage(ctx, ringOut.peerKey, ringOut.packet); } } catch (e) { console.error(e); @@ -82,20 +78,13 @@ export const registerInboundMessageHandler = ( type: number, cb: (ctx: GameContext, v: CursorView, peerId: string) => void ) => { - // TODO: hold a list of multiple handlers + if (network.messageHandlers[type]) { + throw new Error("Cannot re-register more than one inbound network message handlers."); + } network.messageHandlers[type] = cb; }; export function InboundNetworkSystem(ctx: GameContext) { const network = getModule(ctx, NetworkModule); - - // only recieve updates when: - // - we have connected peers - // - player rig has spawned - const haveConnectedPeers = network.peers.length > 0; - const spawnedPlayerRig = ownedPlayerQuery(ctx.world).length > 0; - - if (haveConnectedPeers && spawnedPlayerRig) { - processNetworkMessages(ctx, network); - } + processNetworkMessages(ctx, network); } diff --git a/src/engine/network/NetworkAction.ts b/src/engine/network/NetworkAction.ts deleted file mode 100644 index d3756035b..000000000 --- a/src/engine/network/NetworkAction.ts +++ /dev/null @@ -1,18 +0,0 @@ -export enum NetworkAction { - Create, - Delete, - UpdateChanged, - UpdateSnapshot, - FullChanged, - FullSnapshot, - Prefab, - InformPlayerNetworkId, - NewPeerSnapshot, - RemoveOwnershipMessage, - UpdateNetworkId, - BinaryScriptMessage, - StringScriptMessage, - InformXRMode, -} - -export const UnreliableNetworkActions = [NetworkAction.UpdateChanged, NetworkAction.UpdateSnapshot]; diff --git a/src/engine/network/NetworkComponents.ts b/src/engine/network/NetworkComponents.ts index afdc6a336..4486b8b60 100644 --- a/src/engine/network/NetworkComponents.ts +++ b/src/engine/network/NetworkComponents.ts @@ -2,16 +2,16 @@ import { defineComponent, Types } from "bitecs"; import { maxEntities } from "../config.common"; -/* Components */ - +/** + * Indicates that the entity exists as a networked entity. + */ export const Networked = defineComponent( { - // networkId contains both peerIdIndex (owner) and localNetworkId - networkId: Types.ui32, - // TODO: split net ID into 2 32bit ints - // ownerId: Types.ui32, - // localId: Types.ui32, - parent: Types.ui32, + networkId: Types.ui32, // TODO: make ui64 + authorId: Types.ui32, // TODO: make ui64 + replicatorId: Types.ui32, + lastUpdate: Types.f64, + destroyOnLeave: Types.ui8, position: [Types.f32, 3], quaternion: [Types.f32, 4], velocity: [Types.f32, 3], @@ -19,4 +19,27 @@ export const Networked = defineComponent( maxEntities ); -export const Owned = defineComponent(); +/** + * Indicates that the current peer's simulation is responsible for dictating source-of-truth updates about the entity. + */ +export const Authoring = defineComponent(); + +/** + * Indicates that the current peer is only responsible for relaying the source-of-truth which is being received by the authoring peer. + */ +export const Relaying = defineComponent( + { + for: Types.ui32, // TODO: make ui64 + }, + maxEntities +); + +/** + * Indicates that an entity's authoring peer should be changed + */ +export const TransferAuthority = defineComponent( + { + to: Types.ui32, // TODO: make ui64 + }, + maxEntities +); diff --git a/src/engine/network/NetworkInterpolationSystem.ts b/src/engine/network/NetworkInterpolationSystem.ts index 564f3e1cb..7fb98c9aa 100644 --- a/src/engine/network/NetworkInterpolationSystem.ts +++ b/src/engine/network/NetworkInterpolationSystem.ts @@ -4,8 +4,8 @@ import { Vector3, Quaternion } from "three"; import { quat, vec3 } from "gl-matrix"; import { GameContext } from "../GameTypes"; -import { GameNetworkState, getPeerIndexFromNetworkId, NetworkModule, ownedPlayerQuery } from "./network.game"; -import { Networked, Owned } from "./NetworkComponents"; +import { GameNetworkState, getPeerInfoById, NetworkModule, ownedPlayerQuery, tryGetPeerInfoById } from "./network.game"; +import { Networked, Authoring } from "./NetworkComponents"; import { getModule } from "../module/module.common"; import { INTERP_BUFFER_MS, @@ -20,17 +20,11 @@ import { RemoteNode } from "../resource/RemoteResources"; import { OurPlayer } from "../player/Player"; import { clamp } from "../common/math"; -export const remoteEntityQuery = defineQuery([Networked, Not(Owned), Not(OurPlayer)]); +export const remoteEntityQuery = defineQuery([Networked, Not(Authoring), Not(OurPlayer)]); export const enteredRemoteEntityQuery = enterQuery(remoteEntityQuery); export const exitedRemoteEntityQuery = exitQuery(remoteEntityQuery); -const getPeerIdFromEntityId = (network: GameNetworkState, eid: number) => { - const pidx = getPeerIndexFromNetworkId(Networked.networkId[eid]); - const peerId = network.indexToPeerId.get(pidx) || network.entityIdToPeerId.get(eid); - return peerId; -}; - const _vec = new Vector3(); const _quat = new Quaternion(); @@ -57,15 +51,16 @@ export function NetworkInterpolationSystem(ctx: GameContext) { applyNetworkedToEntity(node, body); // add to historian - const pidx = getPeerIndexFromNetworkId(Networked.networkId[eid]); - const peerId = network.indexToPeerId.get(pidx); + const pid = BigInt(Networked.authorId[eid]); - if (!peerId) { - throw new Error("peer not found for entity " + eid + " peerIndex " + pidx); - } - const historian = network.peerIdToHistorian.get(peerId); + getPeerInfoById; + + const peerInfo = tryGetPeerInfoById(network, pid); + const historian = peerInfo.historian; if (!historian) { - throw new Error("historian not found for peer " + peerId); + // throw new Error("historian not found for peer " + peerId); + console.warn("historian not found for peer " + peerInfo.key); + continue; } addEntityToHistorian(historian, eid); } @@ -91,17 +86,12 @@ export function NetworkInterpolationSystem(ctx: GameContext) { continue; } - const peerId = getPeerIdFromEntityId(network, eid); - if (peerId === undefined) { - console.warn("could not find peerId for:", eid); - continue; - } + const peerInfo = tryGetPeerInfoById(network, BigInt(Networked.authorId[eid])); - const historian = network.peerIdToHistorian.get(peerId); + const historian = peerInfo.historian; if (historian === undefined) { - console.warn("could not find historian for:", peerId); + console.warn("could not find historian for:", peerInfo.key); applyNetworkedToEntity(node, body); - // console.warn("could not find historian for:", peerId); continue; } @@ -161,10 +151,9 @@ export function NetworkInterpolationSystem(ctx: GameContext) { for (let i = 0; i < exited.length; i++) { const eid = exited[i]; // remove from historian - const pidx = getPeerIndexFromNetworkId(Networked.networkId[eid]); - const peerId = network.indexToPeerId.get(pidx); - if (!peerId) continue; - const historian = network.peerIdToHistorian.get(peerId); + const pid = BigInt(Networked.authorId[eid]); + const peerInfo = tryGetPeerInfoById(network, pid); + const historian = peerInfo.historian; if (!historian) continue; removeEntityFromHistorian(historian, eid); } @@ -173,7 +162,11 @@ export function NetworkInterpolationSystem(ctx: GameContext) { } function preprocessHistorians(ctx: GameContext, network: GameNetworkState) { - for (const [, historian] of network.peerIdToHistorian) { + for (const { historian } of network.peers) { + if (!historian) { + continue; + } + if (historian.needsUpdate) { historian.latency = Date.now() - historian.latestTime; // add timestamp to historian @@ -222,7 +215,10 @@ function preprocessHistorians(ctx: GameContext, network: GameNetworkState) { } function postprocessHistorians(ctx: GameContext, network: GameNetworkState) { - for (const [, historian] of network.peerIdToHistorian) { + for (const { historian } of network.peers) { + if (!historian) { + continue; + } historian.needsUpdate = false; } } diff --git a/src/engine/network/NetworkMessage.test.ts b/src/engine/network/NetworkMessage.test.ts new file mode 100644 index 000000000..626cb4987 --- /dev/null +++ b/src/engine/network/NetworkMessage.test.ts @@ -0,0 +1,184 @@ +import { assert } from "vitest"; + +import { + readDespawn, + readPeerInfo, + readSpawn, + readTransform, + writeDespawn, + writePeerInfo, + writeSpawn, + writeTransform, +} from "./NetworkMessage"; +import { RemoteNode, RemotePhysicsBody } from "../resource/RemoteResources"; +import { CursorView, createCursorView, moveCursorView, readFloat32 } from "../allocator/CursorView"; +import { NetworkID, NetworkModule, PeerID, tryGetPeerId } from "./network.game"; +import { NetworkReplicator, createNetworkReplicator } from "./NetworkReplicator"; +import { mockGameState } from "../../../test/engine/mocks"; +import { getModule } from "../module/module.common"; +import { Codec } from "./Codec"; +import { Networked } from "./NetworkComponents"; +import { PhysicsModule, addPhysicsBody } from "../physics/physics.game"; + +const transformCodec: Codec = { + encode: (view: CursorView, node: RemoteNode) => { + return writeTransform(view, node); + }, + decode: (view: CursorView, node: RemoteNode) => { + return readTransform(view, node); + }, +}; + +describe("Network Message Tests", () => { + describe("Unit Tests", () => { + test("writeTransform", () => { + const writer = createCursorView(); + const state = mockGameState(); + const node = new RemoteNode(state.resourceManager); + const physics = getModule(state, PhysicsModule); + + addPhysicsBody(state.world, physics, node, new RemotePhysicsBody(state.resourceManager)); + + node.position.set([1, 2, 3]); + node.quaternion.set([4, 5, 6, 1]); + + const velocity = node.physicsBody!.velocity; + velocity.set([4, 5, 6]); + + writeTransform(writer, node); + + const reader = createCursorView(writer.buffer); + + const posX = readFloat32(reader); + assert.strictEqual(posX, 1); + + const posY = readFloat32(reader); + assert.strictEqual(posY, 2); + + const posZ = readFloat32(reader); + assert.strictEqual(posZ, 3); + + const velX = readFloat32(reader); + assert.strictEqual(velX, 4); + + const velY = readFloat32(reader); + assert.strictEqual(velY, 5); + + const velZ = readFloat32(reader); + assert.strictEqual(velZ, 6); + + const rotX = readFloat32(reader); + assert.strictEqual(rotX, 4); + + const rotY = readFloat32(reader); + assert.strictEqual(rotY, 5); + + const rotZ = readFloat32(reader); + assert.strictEqual(rotZ, 6); + }); + }); + + describe("Integration Tests", () => { + test("writeTransform and readTransform", () => { + const ctx = mockGameState(); + + const mockNode = new RemoteNode(ctx.resourceManager, { + position: [1, 2, 3], + quaternion: [7, 8, 9, 10], + }); + mockNode.physicsBody = new RemotePhysicsBody(ctx.resourceManager); + mockNode.physicsBody.velocity = new Float32Array([4, 5, 6]); + + const v = createCursorView(); + + const writtenBytes = writeTransform(v, mockNode); + + const newNode = new RemoteNode(ctx.resourceManager); + newNode.physicsBody = new RemotePhysicsBody(ctx.resourceManager); + newNode.physicsBody.velocity = new Float32Array([0, 0, 0]); + + moveCursorView(v, 0); + readTransform(v, newNode); + + assert.deepEqual(writtenBytes, 10 * Float32Array.BYTES_PER_ELEMENT); + assert.deepEqual(newNode.position, mockNode.position); + assert.deepEqual(newNode.physicsBody!.velocity, mockNode.physicsBody!.velocity); + assert.deepEqual(newNode.quaternion, mockNode.quaternion); + }); + + test("writePeerInfo and readPeerInfo", () => { + const ctx = mockGameState(); + const network = getModule(ctx, NetworkModule); + + const v = createCursorView(); + + const mockPeerId = "testPeerId"; + const mockPeerIndex = 1n; + + writePeerInfo(v, mockPeerId, mockPeerIndex); + + moveCursorView(v, 0); + readPeerInfo(network, v); + + assert.deepEqual(tryGetPeerId(network, mockPeerId), mockPeerIndex); + }); + + test("writeSpawn and readSpawn", () => { + const ctx = mockGameState(); + const network = getModule(ctx, NetworkModule); + + const replicator: NetworkReplicator = createNetworkReplicator( + ctx, + () => { + const node = new RemoteNode(ctx.resourceManager); + node.physicsBody = new RemotePhysicsBody(ctx.resourceManager); + node.physicsBody.velocity = new Float32Array(); + return node; + }, + transformCodec + ); + + const mockNetworkId: NetworkID = 1n; + const mockauthorId: PeerID = 1n; + const mockData = new Uint8Array([1, 2, 3, 4]).buffer; + const mockNode = replicator.spawn(ctx); + + const v: CursorView = createCursorView(); + + writeSpawn(v, mockNetworkId, mockauthorId, replicator, mockNode, mockData); + + moveCursorView(v, 0); + readSpawn(ctx, network, v); + + assert.ok(network.networkIdToEntityId.has(mockNetworkId)); + assert.ok(replicator.spawned.some((spawn) => spawn.node === mockNode)); + }); + + test("writeDespawn and readDespawn", () => { + const ctx = mockGameState(); + const network = getModule(ctx, NetworkModule); + + const replicator: NetworkReplicator = createNetworkReplicator( + ctx, + () => new RemoteNode(ctx.resourceManager), + transformCodec + ); + + const mockNode = replicator.factory(ctx); + const eid = mockNode.eid; + const nid = BigInt(eid); + + network.networkIdToEntityId.set(nid, eid); + Networked.replicatorId[eid] = replicator.id; + + const v: CursorView = createCursorView(); + + writeDespawn(v, nid); + + moveCursorView(v, 0); + readDespawn(ctx, network, v); + + assert.ok(replicator.despawned.some((despawn) => despawn === mockNode)); + }); + }); +}); diff --git a/src/engine/network/NetworkMessage.ts b/src/engine/network/NetworkMessage.ts new file mode 100644 index 000000000..59b2a8ba8 --- /dev/null +++ b/src/engine/network/NetworkMessage.ts @@ -0,0 +1,562 @@ +import { addComponent, hasComponent } from "bitecs"; + +import { GameContext } from "../GameTypes"; +import { + CursorView, + writeUint64, + writeString, + readUint64, + readString, + writeFloat64, + writeUint16, + sliceCursorView, + readFloat64, + readUint16, + readUint32, + writeUint8, + readUint8, + readFloat32, + scrollCursorView, + spaceUint16, + writeFloat32, + writePropIfChanged, + writeUint32, + readArrayBuffer, + writeArrayBuffer, + spaceUint64, + moveCursorView, + spaceUint32, +} from "../allocator/CursorView"; +import { getModule } from "../module/module.common"; +import { RemoteNode, removeObjectFromWorld } from "../resource/RemoteResources"; +import { getRemoteResource, tryGetRemoteResource } from "../resource/resource.game"; +import { checkBitflag } from "../utils/checkBitflag"; +import { Authoring, Networked } from "./NetworkComponents"; +import { NetworkReplicator, tryGetNetworkReplicator } from "./NetworkReplicator"; +import { + GameNetworkState, + authoringNetworkedQuery, + NetworkModule, + spawnedNetworkeQuery, + despawnedNetworkQuery, + relayingNetworkedQuery, + removePeerInfo, + NetworkID, + PeerID, + addPeerInfo, + getPeerInfoByKey, + PeerInfo, +} from "./network.game"; +import { Codec } from "./Codec"; + +export enum NetworkMessage { + // old + Create, + Delete, + UpdateChanged, + UpdateSnapshot, + FullChanged, + FullSnapshot, + Prefab, + InformPlayerNetworkId, + NewPeerSnapshot, + RemoveOwnershipMessage, + UpdateNetworkId, + BinaryScriptMessage, + StringScriptMessage, + InformXRMode, + + // new + Spawn, + Despawn, + HostSnapshot, + PeerEntered, + PeerExited, + HostCommands, + PeerCommands, + EntityUpdates, +} + +export const UnreliableNetworkActions = [ + // old + NetworkMessage.UpdateChanged, + NetworkMessage.UpdateSnapshot, + + // new + NetworkMessage.EntityUpdates, +]; + +export const writeMessageType = (v: CursorView, type: NetworkMessage) => writeUint8(v, type); +export const readMessageType = (v: CursorView) => readUint8(v); + +/* Transform serialization */ + +export const writeTransform = (v: CursorView, node: RemoteNode) => { + const cursorBefore = v.cursor; + + const position = node.position; + writeFloat32(v, position[0]); + writeFloat32(v, position[1]); + writeFloat32(v, position[2]); + + const velocity = node.physicsBody!.velocity; + writeFloat32(v, velocity[0]); + writeFloat32(v, velocity[1]); + writeFloat32(v, velocity[2]); + + const quaternion = node.quaternion; + writeFloat32(v, quaternion[0]); + writeFloat32(v, quaternion[1]); + writeFloat32(v, quaternion[2]); + writeFloat32(v, quaternion[3]); + + return v.cursor - cursorBefore; +}; + +export const readTransform = (v: CursorView, node: RemoteNode) => { + const position = node.position; + position[0] = readFloat32(v); + position[1] = readFloat32(v); + position[2] = readFloat32(v); + + const velocity = node.physicsBody!.velocity; + velocity[0] = readFloat32(v); + velocity[1] = readFloat32(v); + velocity[2] = readFloat32(v); + + const quaternion = node.quaternion; + quaternion[0] = readFloat32(v); + quaternion[1] = readFloat32(v); + quaternion[2] = readFloat32(v); + quaternion[3] = readFloat32(v); +}; + +export const transformCodec: Codec = { + encode: writeTransform, + decode: readTransform, +}; + +export const writeTransformMutations = (v: CursorView, node: RemoteNode) => { + const writeChangeMask = spaceUint16(v); + let changeMask = 0; + let b = 0; + + const position = node.position; + changeMask |= writePropIfChanged(v, position, 0) ? 1 << b++ : b++ && 0; + changeMask |= writePropIfChanged(v, position, 1) ? 1 << b++ : b++ && 0; + changeMask |= writePropIfChanged(v, position, 2) ? 1 << b++ : b++ && 0; + + const velocity = node.physicsBody!.velocity; + changeMask |= writePropIfChanged(v, velocity, 0) ? 1 << b++ : b++ && 0; + changeMask |= writePropIfChanged(v, velocity, 1) ? 1 << b++ : b++ && 0; + changeMask |= writePropIfChanged(v, velocity, 2) ? 1 << b++ : b++ && 0; + + const quaternion = node.quaternion; + changeMask |= writePropIfChanged(v, quaternion, 0) ? 1 << b++ : b++ && 0; + changeMask |= writePropIfChanged(v, quaternion, 1) ? 1 << b++ : b++ && 0; + changeMask |= writePropIfChanged(v, quaternion, 2) ? 1 << b++ : b++ && 0; + changeMask |= writePropIfChanged(v, quaternion, 3) ? 1 << b++ : b++ && 0; + + writeChangeMask(changeMask); + + return changeMask > 0; +}; + +export const readTransformMutations = (v: CursorView, nid: NetworkID, node: RemoteNode | undefined) => { + if (node) { + const changeMask = readUint16(v); + let b = 0; + + const position = Networked.position[node.eid]; + if (checkBitflag(changeMask, 1 << b++)) position[0] = readFloat32(v); + if (checkBitflag(changeMask, 1 << b++)) position[1] = readFloat32(v); + if (checkBitflag(changeMask, 1 << b++)) position[2] = readFloat32(v); + + const velocity = Networked.velocity[node.eid]; + if (checkBitflag(changeMask, 1 << b++)) velocity[0] = readFloat32(v); + if (checkBitflag(changeMask, 1 << b++)) velocity[1] = readFloat32(v); + if (checkBitflag(changeMask, 1 << b++)) velocity[2] = readFloat32(v); + + const quaternion = Networked.quaternion[node.eid]; + if (checkBitflag(changeMask, 1 << b++)) quaternion[0] = readFloat32(v); + if (checkBitflag(changeMask, 1 << b++)) quaternion[1] = readFloat32(v); + if (checkBitflag(changeMask, 1 << b++)) quaternion[2] = readFloat32(v); + if (checkBitflag(changeMask, 1 << b++)) quaternion[3] = readFloat32(v); + } else { + console.warn(`could not deserialize transform update for non-existent entity for networkID ${nid}`); + scrollCursorView(v, Float32Array.BYTES_PER_ELEMENT * 10 + Uint32Array.BYTES_PER_ELEMENT); + } +}; + +// PeerInfo +export const writePeerInfo = (v: CursorView, peerKey: string, peerId: PeerID) => { + console.log("writePeerInfo ========"); + console.log("peerId", peerId); + console.log("peerKey", peerKey); + writeUint64(v, peerId); + writeString(v, peerKey); +}; +export const readPeerInfo = (network: GameNetworkState, v: CursorView, hostPeerId?: PeerID, localPeerId?: PeerID) => { + const peerId = readUint64(v); + const peerKey = readString(v); + + console.log("readPeerInfo ========="); + console.log("hostPeerId", hostPeerId); + console.log("localPeerId", localPeerId); + console.log("peerId", peerId); + console.log("peerKey", peerKey); + + const peerInfo = addPeerInfo(network, peerKey, peerId); + + if (peerId === hostPeerId) { + console.log("setting host peer", peerInfo); + network.host = peerInfo; + } + if (peerId === localPeerId) { + console.log("setting local peer", peerInfo); + network.local = peerInfo; + } + + if (peerId > network.peerIdCount) { + // this keeps peerIdCount synchronized on all peers + // set count to one increment ahead of this peerId + network.peerIdCount = peerId + 1n; + } + + return peerInfo; +}; + +// Spawn +export const writeSpawn = ( + v: CursorView, + networkId: NetworkID, + authorId: PeerID, + replicator: NetworkReplicator, + node: RemoteNode, + data?: ArrayBuffer +) => { + writeUint64(v, networkId); + writeUint64(v, authorId); + writeUint32(v, replicator.id); + writeUint32(v, data?.byteLength || 0); + if (data && data.byteLength > 0) writeArrayBuffer(v, data); + replicator.snapshotCodec.encode(v, node); +}; + +export const readSpawn = (ctx: GameContext, network: GameNetworkState, v: CursorView) => { + // read + const networkId = readUint64(v); + const authorId = readUint64(v); + const replicatorId = readUint32(v); + const dataByteLength = readUint32(v); + const data = dataByteLength > 0 ? readArrayBuffer(v, dataByteLength) : undefined; + + // effect + const replicator = tryGetNetworkReplicator(network, replicatorId); + const node = replicator.factory(ctx); + + // read + replicator.snapshotCodec.decode(v, node); + + // HACK: have to remove object after decoding so that the view cursor is moved forward + if (network.networkIdToEntityId.has(networkId)) { + console.warn("Attempted to spawn an already exiting entity with networkId:", networkId); + removeObjectFromWorld(ctx, node); + return; + } + + network.networkIdToEntityId.set(networkId, node.eid); + + // effect + addComponent(ctx.world, Networked, node.eid); + Networked.replicatorId[node.eid] = replicatorId; + Networked.networkId[node.eid] = Number(networkId); + Networked.authorId[node.eid] = Number(authorId); + + replicator.spawned.push({ node, data }); + + // this keeps networkIdCount synchronized on all peers + if (networkId > network.networkIdCount) { + network.networkIdCount = networkId + 1n; + } +}; + +// Despawn +export const writeDespawn = (v: CursorView, nid: NetworkID) => { + writeUint64(v, nid); +}; +export const readDespawn = (ctx: GameContext, network: GameNetworkState, v: CursorView) => { + const nid = readUint64(v); + const eid = network.networkIdToEntityId.get(nid)!; + const node = tryGetRemoteResource(ctx, eid); + + const replicator = tryGetNetworkReplicator(network, Networked.replicatorId[eid]); + replicator.despawned.enqueue(node); +}; + +// Update +export const writeUpdate = ( + v: CursorView, + networkId: NetworkID, + replicator: NetworkReplicator, + node: RemoteNode +) => { + const writeNetworkId = spaceUint64(v); + const writeBytes = spaceUint32(v); + const bytesWritten = replicator.mutationCodec.encode(v, node); + if (bytesWritten) { + writeNetworkId(networkId); + writeBytes(bytesWritten); + } + return bytesWritten > 0; +}; + +export const readUpdate = (ctx: GameContext, network: GameNetworkState, v: CursorView) => { + const networkId = readUint64(v); + const bytesWritten = readUint32(v); + + const eid = network.networkIdToEntityId.get(networkId) || 0; + const node = getRemoteResource(ctx, eid); + const replicatorId = Networked.replicatorId[eid]; + const replicator = tryGetNetworkReplicator(network, replicatorId); + + // ignore update if node doesn't exist, or we are the author of this entity + if (!node || hasComponent(ctx.world, Authoring, eid)) { + scrollCursorView(v, bytesWritten); + return; + } + + replicator.mutationCodec.decode(v, node); +}; + +// Authority Transfer +// const writeAuthorityTransfer = (v: CursorView, nid: NetworkID, authorId: PeerIndex) => { +// writeUint64(v, nid); +// writeUint64(v, authorId); +// }; +// const readAuthorityTransfer = (network: GameNetworkState, v: CursorView) => { +// const nid = readUint64(v); +// const authorId = readUint64(v); + +// const eid = network.networkIdToEntityId.get(nid); +// if (!eid) { +// console.warn("Unable to transfer authority, could not find entity for nid: " + nid); +// return; +// } + +// Networked.authorId[eid] = Number(authorId); +// }; + +// HostSnapshot Message +export const serializeHostSnapshot = (ctx: GameContext, network: GameNetworkState, peer: PeerInfo) => { + const v = network.cursorView; + + writeMessageType(v, NetworkMessage.HostSnapshot); + + const time = performance.now(); + const { host } = network; + + writeFloat64(v, time); + writeUint64(v, peer!.id!); + writeUint64(v, host!.id!); + + const peerCount = network.peers.length; + writeUint16(v, peerCount); + for (let i = 0; i < peerCount; i++) { + const peerInfo = network.peers[i]; + writePeerInfo(v, peerInfo.key, peerInfo.id!); + } + + // TODO: handle host migration cases + const spawned = authoringNetworkedQuery(ctx.world); + const spawnedRelaying = relayingNetworkedQuery(ctx.world); + writeUint16(v, spawned.length + spawnedRelaying.length); + for (let i = 0; i < spawned.length; i++) { + const eid = spawned[i]; + const nid = BigInt(Networked.networkId[eid]); + const node = tryGetRemoteResource(ctx, eid); + const replicator = tryGetNetworkReplicator(network, Networked.replicatorId[eid]); + const authorId = BigInt(Networked.authorId[eid]); + writeSpawn(v, nid, authorId, replicator, node); + } + // TODO: compress with above + for (let i = 0; i < spawnedRelaying.length; i++) { + const eid = spawnedRelaying[i]; + const nid = BigInt(Networked.networkId[eid]); + const node = tryGetRemoteResource(ctx, eid); + const replicator = tryGetNetworkReplicator(network, Networked.replicatorId[eid]); + const authorId = BigInt(Networked.authorId[eid]); + writeSpawn(v, nid, authorId, replicator, node); + } + + return sliceCursorView(v); +}; + +export const deserializeHostSnapshot = (ctx: GameContext, v: CursorView) => { + const network = getModule(ctx, NetworkModule); + + readFloat64(v); // TOOD: handle time + const localPeerId = readUint64(v); + const hostPeerId = readUint64(v); + + const peerCount = readUint16(v); + for (let i = 0; i < peerCount; i++) { + readPeerInfo(network, v, hostPeerId, localPeerId); + } + + const spawnCount = readUint16(v); + for (let i = 0; i < spawnCount; i++) { + readSpawn(ctx, network, v); + } +}; + +// PeerEntered Message +export const serializePeerEntered = (ctx: GameContext, network: GameNetworkState, peerKey: string, peerId: PeerID) => { + const v = network.cursorView; + writeMessageType(v, NetworkMessage.PeerEntered); + writePeerInfo(v, peerKey, peerId); + return sliceCursorView(v); +}; +export const deserializePeerEntered = (ctx: GameContext, v: CursorView) => { + const network = getModule(ctx, NetworkModule); + readPeerInfo(network, v, network.host?.id, network.local?.id); +}; + +// PeerExited Message +export const serializePeerExited = (ctx: GameContext, network: GameNetworkState, peerIndex: PeerID) => { + const v = network.cursorView; + writeMessageType(v, NetworkMessage.PeerExited); + writeUint64(v, peerIndex); + return sliceCursorView(v); +}; +export const deserializePeerExited = (ctx: GameContext, v: CursorView) => { + const network = getModule(ctx, NetworkModule); + const peerId = readUint64(v); + const peerInfo = network.peerIdToInfo.get(peerId)!; + removePeerInfo(network, peerInfo.key); +}; + +// HostCommands Message +export const serializeHostCommands = (ctx: GameContext, network: GameNetworkState) => { + const spawned = spawnedNetworkeQuery(ctx.world); + const despawned = despawnedNetworkQuery(ctx.world); + if (spawned.length === 0 && despawned.length === 0) { + // TODO: return and handle undefined + return new ArrayBuffer(0); + } + + const v = network.cursorView; + + writeMessageType(v, NetworkMessage.HostCommands); + + writeUint16(v, spawned.length); + for (let i = 0; i < spawned.length; i++) { + const eid = spawned[i]; + const networkId = BigInt(Networked.networkId[eid]); + const authorId = BigInt(Networked.authorId[eid]); + const replicatorId = Networked.replicatorId[eid]; + + const node = tryGetRemoteResource(ctx, eid); + const replicator = tryGetNetworkReplicator(network, replicatorId); + + writeSpawn(v, networkId, authorId, replicator, node); + } + + writeUint16(v, despawned.length); + for (let i = 0; i < despawned.length; i++) { + const eid = despawned[i]; + const nid = BigInt(Networked.networkId[eid]); + writeDespawn(v, nid); + } + + // TODO: implement transferAuthority(to) which simply adds a TransferAuthority component + // const authorityTransfers = authorityTransferQuery(ctx.world); + // writeUint32(v, authorityTransferQuery.length); + // for (let i = 0; i < authorityTransferQuery.length; i++) { + // const authorityTransfer = authorityTransferQuery[i]; + // writeAuthorityTransfer(v, authorityTransfer); + // } + + return sliceCursorView(v); +}; + +export const deserializeHostCommands = (ctx: GameContext, v: CursorView) => { + const network = getModule(ctx, NetworkModule); + + const spawnCount = readUint16(v); + for (let i = 0; i < spawnCount; i++) { + readSpawn(ctx, network, v); + } + + const despawnCount = readUint16(v); + for (let i = 0; i < despawnCount; i++) { + readDespawn(ctx, network, v); + } + + // const authorityTransferCount = readUint32(v); + // for (let i = 0; i < authorityTransferCount; i++) { + // readAuthorityTransfer(network, v); + // } +}; + +// EntityUpdates message +export const serializeEntityUpdates = (ctx: GameContext, network: GameNetworkState) => { + const v = network.cursorView; + writeMessageType(v, NetworkMessage.EntityUpdates); + + const time = performance.now(); + writeFloat64(v, time); + + const authors = authoringNetworkedQuery(ctx.world); + let count = 0; + const writeCount = spaceUint16(v); + for (let i = 0; i < authors.length; i++) { + const eid = authors[i]; + const networkId = BigInt(Networked.networkId[eid]); + const replicatorId = Networked.replicatorId[eid]; + const replicator = tryGetNetworkReplicator(network, replicatorId); + const node = tryGetRemoteResource(ctx, eid); + + count += writeUpdate(v, networkId, replicator, node) ? 1 : 0; + } + const relays = relayingNetworkedQuery(ctx.world); + for (let i = 0; i < relays.length; i++) { + const eid = relays[i]; + const networkId = BigInt(Networked.networkId[eid]); + const replicatorId = Networked.replicatorId[eid]; + const replicator = tryGetNetworkReplicator(network, replicatorId); + const node = tryGetRemoteResource(ctx, eid); + + count += writeUpdate(v, networkId, replicator, node) ? 1 : 0; + } + + if (count) { + writeCount(count); + } else { + moveCursorView(v, 0); + } + + return sliceCursorView(v); +}; + +export const deserializeEntityUpdates = (ctx: GameContext, v: CursorView, peerKey: string) => { + const network = getModule(ctx, NetworkModule); + + const time = readFloat64(v); + + const peerInfo = getPeerInfoByKey(network, peerKey); + if (!peerInfo) { + return; + } + + const lastTime = peerInfo.lastUpdate || 0; + peerInfo.lastUpdate = time; + if (time < lastTime) { + return; + } + + const count = readUint16(v); + for (let i = 0; i < count; i++) { + readUpdate(ctx, network, v); + } +}; diff --git a/src/engine/network/NetworkReplicator.ts b/src/engine/network/NetworkReplicator.ts new file mode 100644 index 000000000..fba6d0ae0 --- /dev/null +++ b/src/engine/network/NetworkReplicator.ts @@ -0,0 +1,103 @@ +import { addComponent } from "bitecs"; + +import { getModule } from "../module/module.common"; +import { RemoteNode } from "../resource/RemoteResources"; +import { GameNetworkState, NetworkModule, createNetworkId } from "./network.game"; +import { Networked } from "./NetworkComponents"; +import { GameContext } from "../GameTypes"; +import { Queue, createQueue } from "../utils/Queue"; +import { Codec } from "./Codec"; +import { isHost } from "./network.common"; + +export interface NetworkReplication { + node: RemoteNode; + data?: ArrayBuffer; +} + +export type NetworkReplicatorFactory = (ctx: GameContext, data?: ArrayBuffer) => RemoteNode; + +export interface NetworkReplicator { + id: number; + spawned: Queue; + despawned: Queue; + instances: RemoteNode[]; + snapshotCodec: Codec; + mutationCodec: Codec; + factory: NetworkReplicatorFactory; + spawn: NetworkReplicatorFactory; + despawn: (node: RemoteNode) => void; +} + +export const createNetworkReplicator = ( + ctx: GameContext, + factory: NetworkReplicatorFactory, + snapshotCodec: Codec, + mutationCodec?: Codec +): NetworkReplicator => { + const network = getModule(ctx, NetworkModule); + + const id = network.replicatorIdCount++; + + const instances: RemoteNode[] = []; + const spawned = createQueue(); + const despawned = createQueue(); + + const spawn = (ctx: GameContext, data?: ArrayBuffer) => { + if (!isHost(network)) { + throw new Error("Only hosts can spawn items."); + } + + const node = factory(ctx, data); + const eid = node.eid; + + addComponent(ctx.world, Networked, eid); + const networkId = createNetworkId(network); + Networked.networkId[eid] = Number(networkId); + network.networkIdToEntityId.set(networkId, eid); + Networked.replicatorId[eid] = id; + Networked.destroyOnLeave[eid] = 1; + + spawned.enqueue({ + node, + data, + }); + + instances.push(node); + + return node; + }; + + const despawn = (node: RemoteNode) => { + if (!isHost(network)) { + throw new Error("Only hosts can despawn items."); + } + despawned.enqueue(node); + instances.splice(instances.indexOf(node), 1); + }; + + const replicator: NetworkReplicator = { + id, + instances, + spawned, + despawned, + spawn, + despawn, + factory, + snapshotCodec, + mutationCodec: mutationCodec || snapshotCodec, + }; + + network.replicators.set(id, replicator); + + return replicator; +}; + +export const tryGetNetworkReplicator = (network: GameNetworkState, id: number) => { + const replicator = network.replicators.get(id); + if (!replicator) { + throw new Error("Replicator not found for replicatorId: " + id); + } + return replicator as NetworkReplicator; +}; +export const getNetworkReplicator = (network: GameNetworkState, id: number) => + network.replicators.get(id) as NetworkReplicator | undefined; diff --git a/src/engine/network/RingBuffer.ts b/src/engine/network/NetworkRingBuffer.ts similarity index 51% rename from src/engine/network/RingBuffer.ts rename to src/engine/network/NetworkRingBuffer.ts index e5d1ab6d3..b3eb03eb2 100644 --- a/src/engine/network/RingBuffer.ts +++ b/src/engine/network/NetworkRingBuffer.ts @@ -16,10 +16,10 @@ import { readUint32, readUint8, writeArrayBuffer, - writeString, writeUint32, writeUint8, } from "../allocator/CursorView"; +import { GameNetworkState } from "./network.game"; export interface NetworkRingBuffer extends RingBuffer { buffer: ArrayBuffer; @@ -42,21 +42,23 @@ export function createNetworkRingBuffer(capacity = 1000): NetworkRingBuffer { }); } -const writePeerIdCache = new Map(); -const writePeerId = (v: CursorView, peerId: string) => { - const encoded = writePeerIdCache.get(peerId); - if (encoded) { - writeUint8(v, encoded.byteLength); - writeArrayBuffer(v, encoded); - } else { - writeString(v, peerId); +const textEncoder = new TextEncoder(); +const writePeerKeyCache = new Map(); +const writePeerKey = (v: CursorView, peerKey: string) => { + let encoded = writePeerKeyCache.get(peerKey); + if (!encoded) { + encoded = textEncoder.encode(peerKey); + writePeerKeyCache.set(peerKey, encoded); } + + writeUint8(v, encoded.byteLength); + writeArrayBuffer(v, encoded); return v; }; export function enqueueNetworkRingBuffer( rb: NetworkRingBuffer, - peerId: string, + peerKey: string, packet: ArrayBuffer, broadcast = false ) { @@ -65,7 +67,7 @@ export function enqueueNetworkRingBuffer( moveCursorView(view, 0); // TODO: write peerIndex instead - writePeerId(view, peerId); + writePeerKey(view, peerKey); writeUint8(view, broadcast ? 1 : 0); @@ -81,7 +83,7 @@ export function enqueueNetworkRingBuffer( export function dequeueNetworkRingBuffer( rb: NetworkRingBuffer, - out: { packet: ArrayBuffer; peerId: string; broadcast: boolean } + out: { packet: ArrayBuffer; peerKey: string; broadcast: boolean } ) { if (isRingBufferEmpty(rb)) { return false; @@ -91,7 +93,7 @@ export function dequeueNetworkRingBuffer( const { view } = rb; moveCursorView(view, 0); - out.peerId = readString(view); + out.peerKey = readString(view); out.broadcast = readUint8(view) ? true : false; @@ -100,3 +102,31 @@ export function dequeueNetworkRingBuffer( return rv === rb.array.length; } + +export const enqueueReliableBroadcast = (network: GameNetworkState, packet: ArrayBuffer) => { + if (!packet.byteLength) return; + if (!enqueueNetworkRingBuffer(network.outgoingReliableRingBuffer, "", packet, true)) { + console.warn("outgoing reliable network ring buffer full"); + } +}; + +export const enqueueUnreliableBroadcast = (network: GameNetworkState, packet: ArrayBuffer) => { + if (!packet.byteLength) return; + if (!enqueueNetworkRingBuffer(network.outgoingUnreliableRingBuffer, "", packet, true)) { + console.warn("outgoing unreliable network ring buffer full"); + } +}; + +export const enqueueReliable = (network: GameNetworkState, peerId: string, packet: ArrayBuffer) => { + if (!packet.byteLength) return; + if (!enqueueNetworkRingBuffer(network.outgoingReliableRingBuffer, peerId, packet)) { + console.warn("outgoing reliable network ring buffer full"); + } +}; + +export const enqueueUnreliable = (network: GameNetworkState, peerId: string, packet: ArrayBuffer) => { + if (!packet.byteLength) return; + if (!enqueueNetworkRingBuffer(network.outgoingUnreliableRingBuffer, peerId, packet)) { + console.warn("outgoing unreliable network ring buffer full"); + } +}; diff --git a/src/engine/network/OutboundNetworkSystem.ts b/src/engine/network/OutboundNetworkSystem.ts new file mode 100644 index 000000000..6f878516a --- /dev/null +++ b/src/engine/network/OutboundNetworkSystem.ts @@ -0,0 +1,82 @@ +import { GameContext } from "../GameTypes"; +import { getModule } from "../module/module.common"; +import { isHost } from "./network.common"; +import { NetworkModule, exitedNetworkedQuery, ownedPlayerQuery, GameNetworkState, newPeersQueue } from "./network.game"; +import { Networked } from "./NetworkComponents"; +import { serializeHostSnapshot, serializeHostCommands, serializeEntityUpdates } from "./NetworkMessage"; +import { enqueueReliable, enqueueReliableBroadcast, enqueueUnreliableBroadcast } from "./NetworkRingBuffer"; + +const sendUpdatesHost = (ctx: GameContext, network: GameNetworkState) => { + /** + * Send updates from host if: + * - we have connected peers + */ + + const connectedToPeers = network.peers.length > 1; + if (!connectedToPeers) { + return; + } + + const haveNewPeers = newPeersQueue.length > 0; + if (haveNewPeers) { + let peerInfo; + while ((peerInfo = newPeersQueue.dequeue())) { + // TODO: optimize such that host snapshot isn't re-serialized for each peer + const hostSnapshot = serializeHostSnapshot(ctx, network, peerInfo); + console.log("sending host snapshot to ", peerInfo.key); + enqueueReliable(network, peerInfo.key, hostSnapshot); + } + } + + enqueueReliableBroadcast(network, serializeHostCommands(ctx, network)); + enqueueUnreliableBroadcast(network, serializeEntityUpdates(ctx, network)); +}; + +const sendUpdatesClient = (ctx: GameContext, network: GameNetworkState) => { + /** + * Send updates from client if: + * - we have peer connections + * - we have a host connection + * - host snapshot received + */ + + const connectedToPeers = network.peers.length > 0; + if (!connectedToPeers) { + return; + } + + const connectedToHost = network.host && network.peers.includes(network.host); + if (!connectedToHost) { + return; + } + + const hostSnapshotReceived = ownedPlayerQuery(ctx.world).length > 0 && network.local?.id; + if (!hostSnapshotReceived) { + return; + } + + enqueueReliable(network, network.host!.key, serializeEntityUpdates(ctx, network)); +}; + +export function OutboundNetworkSystem(ctx: GameContext) { + const network = getModule(ctx, NetworkModule); + + try { + if (isHost(network)) { + sendUpdatesHost(ctx, network); + } else { + sendUpdatesClient(ctx, network); + } + } catch (e) { + console.error(e); + } + + // delete networkId to entityId mapping + const exited = exitedNetworkedQuery(ctx.world); + for (let i = 0; i < exited.length; i++) { + const eid = exited[i]; + network.networkIdToEntityId.delete(BigInt(Networked.networkId[eid])); + } + + return ctx; +} diff --git a/src/engine/network/Replicator.ts b/src/engine/network/Replicator.ts index ef673c762..32cf048a4 100644 --- a/src/engine/network/Replicator.ts +++ b/src/engine/network/Replicator.ts @@ -1,10 +1,10 @@ import { RemoteResourceManager } from "../GameTypes"; -import { GameNetworkState } from "./network.game"; +import { GameNetworkState, NetworkID, PeerID } from "./network.game"; export interface Replication { nodeId?: number; - networkId?: number; - peerIndex: number; + networkId?: NetworkID; + peerIndex: PeerID; data?: ArrayBuffer; } diff --git a/src/engine/network/TransferAuthoritySystem.ts b/src/engine/network/TransferAuthoritySystem.ts new file mode 100644 index 000000000..cd8f56a49 --- /dev/null +++ b/src/engine/network/TransferAuthoritySystem.ts @@ -0,0 +1,17 @@ +import { defineQuery } from "bitecs"; + +import { GameContext } from "../GameTypes"; +// import { getModule } from "../module/module.common"; +import { TransferAuthority } from "./NetworkComponents"; +// import { NetworkModule } from "./network.game"; + +const transferAuthorityQuery = defineQuery([TransferAuthority]); + +export function TransferAuthoritySystem(ctx: GameContext) { + // const network = getModule(ctx, NetworkModule); + + const ents = transferAuthorityQuery(ctx.world); + for (let i = 0; i < ents.length; i++) { + // const eid = ents[i]; + } +} diff --git a/src/engine/network/createMatrixNetworkInterface.ts b/src/engine/network/createMatrixNetworkInterface.ts index 9a9f2c1d2..ef16a8837 100644 --- a/src/engine/network/createMatrixNetworkInterface.ts +++ b/src/engine/network/createMatrixNetworkInterface.ts @@ -10,7 +10,7 @@ import { SubscriptionHandle, } from "@thirdroom/hydrogen-view-sdk"; -import { exitWorld } from "../../plugins/thirdroom/thirdroom.main"; +import { enterWorld, exitWorld } from "../../plugins/thirdroom/thirdroom.main"; import { setLocalMediaStream } from "../audio/audio.main"; import { MainContext } from "../MainThread"; import { addPeer, disconnect, hasPeer, removePeer, setHost } from "./network.main"; @@ -95,7 +95,7 @@ export async function createMatrixNetworkInterface( const userId = session.userId; const initialHostId = await getInitialHost(groupCall, userId); - await joinWorld(groupCall, userId, initialHostId === userId); + await joinWorld(groupCall, userId, initialHostId); function getInitialHost(groupCall: GroupCall, userId: string): Promise { // Of the all group call members find the one whose member event is oldest @@ -156,8 +156,8 @@ export async function createMatrixNetworkInterface( }); } - async function joinWorld(groupCall: GroupCall, userId: string, isHost: boolean) { - if (isHost) setHost(ctx, userId); + async function joinWorld(groupCall: GroupCall, userId: string, hostId: string) { + setHost(ctx, hostId); unsubscibeMembersObservable = groupCall.members.subscribe({ onAdd(_key, member) { @@ -186,6 +186,8 @@ export async function createMatrixNetworkInterface( addPeer(ctx, member.userId, member.dataChannel, member.remoteMedia?.userMedia); } } + + await enterWorld(ctx, userId, hostId); } function updateHost(groupCall: GroupCall, userId: string) { diff --git a/src/engine/network/network.common.ts b/src/engine/network/network.common.ts index a0b79ae38..2774b3308 100644 --- a/src/engine/network/network.common.ts +++ b/src/engine/network/network.common.ts @@ -1,7 +1,6 @@ import { Message } from "../module/module.common"; -import { GameNetworkState } from "./network.game"; -import { MainNetworkState } from "./network.main"; -import { NetworkRingBuffer } from "./RingBuffer"; +import { GameNetworkState, PeerID } from "./network.game"; +import { NetworkRingBuffer } from "./NetworkRingBuffer"; export enum NetworkMessageType { // Main -> Game @@ -41,12 +40,13 @@ export interface SetHostMessage extends Message { } export interface PeerEnteredMessage extends Message { - peerIndex: number; + peerIndex: PeerID; } export interface PeerExitedMessage extends Message { - peerIndex: number; + peerIndex: PeerID; } -export const isHost = (network: GameNetworkState | MainNetworkState): boolean => - !!network.peerId && !!network.hostId && network.hostId === network.peerId; +// TODO: move out of common, into game +export const isHost = (network: GameNetworkState): boolean => + !!network.local && !!network.host && network.host.key === network.local.key; diff --git a/src/engine/network/network.game.ts b/src/engine/network/network.game.ts index 952335cfe..6c0a90d99 100644 --- a/src/engine/network/network.game.ts +++ b/src/engine/network/network.game.ts @@ -1,82 +1,107 @@ -import { defineQuery, enterQuery, exitQuery, Not } from "bitecs"; -import murmurHash from "murmurhash-js"; +import { addComponent, defineQuery, enterQuery, exitQuery, Not } from "bitecs"; import { availableRead } from "@thirdroom/ringbuffer"; import { createCursorView, CursorView } from "../allocator/CursorView"; import { GameContext } from "../GameTypes"; -import { Player } from "../player/Player"; +import { OurPlayer } from "../player/Player"; import { defineModule, getModule, registerMessageHandler, Thread } from "../module/module.common"; import { AddPeerIdMessage, InitializeNetworkStateMessage, + isHost, NetworkMessageType, PeerEnteredMessage, PeerExitedMessage, RemovePeerIdMessage, SetHostMessage, } from "./network.common"; -import { deserializeRemoveOwnership } from "./ownership.game"; -import { createHistorian, Historian } from "./Historian"; +import { Historian } from "./Historian"; import { - deserializeCreates, - deserializeDeletes, - deserializeFullChangedUpdate, - deserializeInformPlayerNetworkId, - deserializeInformXRMode, - deserializeNewPeerSnapshot, - deserializeSnapshot, - deserializeUpdateNetworkId, - deserializeUpdatesChanged, - deserializeUpdatesSnapshot, -} from "./serialization.game"; -import { NetworkAction } from "./NetworkAction"; -import { registerInboundMessageHandler } from "./inbound.game"; -import { dequeueNetworkRingBuffer, NetworkRingBuffer } from "./RingBuffer"; + deserializeEntityUpdates, + deserializeHostCommands, + deserializeHostSnapshot, + deserializePeerEntered, + deserializePeerExited, + NetworkMessage, +} from "./NetworkMessage"; +import { registerInboundMessageHandler } from "./InboundNetworkSystem"; +import { dequeueNetworkRingBuffer, NetworkRingBuffer } from "./NetworkRingBuffer"; import { ExitWorldMessage, ThirdRoomMessageType } from "../../plugins/thirdroom/thirdroom.common"; -import { getRemoteResource } from "../resource/resource.game"; -import { RemoteNode, removeObjectFromWorld } from "../resource/RemoteResources"; -import { Networked, Owned } from "./NetworkComponents"; -import { XRMode } from "../renderer/renderer.common"; +import { tryGetRemoteResource } from "../resource/resource.game"; +import { RemoteNode } from "../resource/RemoteResources"; +import { Networked, Authoring, Relaying } from "./NetworkComponents"; import { Replicator } from "./Replicator"; +import { createQueue } from "../utils/Queue"; +import { Message } from "../module/module.common"; +import { NetworkReplicator, tryGetNetworkReplicator } from "./NetworkReplicator"; +import { waitUntil } from "../utils/waitUntil"; +import { ThirdRoomModule } from "../../plugins/thirdroom/thirdroom.game"; +import { XRMode } from "../renderer/renderer.common"; /********* * Types * ********/ +// TODO: nominalize so the types are not interchangeable: bigint & { __brand: "Brand Name" } +export type PeerID = bigint; +export type NetworkID = bigint; + +// TODO: compress relevant properties from GameNetworkState onto PeerInfo +export interface PeerInfo { + key: string; + // TODO: make component on entity + xrMode: XRMode; + lastUpdate: number; + id?: PeerID; + networkId?: NetworkID; + entityId?: number; + historian?: Historian; +} + export interface DeferredUpdate { position: Float32Array; quaternion: Float32Array; } export interface GameNetworkState { - onExitWorldQueue: any[]; - incomingReliableRingBuffer: NetworkRingBuffer; - incomingUnreliableRingBuffer: NetworkRingBuffer; - outgoingReliableRingBuffer: NetworkRingBuffer; - outgoingUnreliableRingBuffer: NetworkRingBuffer; - commands: [number, number, ArrayBuffer][]; - hostId: string; - peerId: string; - peers: string[]; - newPeers: string[]; - peerIdCount: number; - peerIdToIndex: Map; - indexToPeerId: Map; - peerIdToHistorian: Map; - peerIdToEntityId: Map; - peerIdToXRMode: Map; - entityIdToPeerId: Map; - networkIdToEntityId: Map; + // NetworkReplicator + replicatorIdCount: number; + replicators: Map>; + + // ScriptReplicator + prefabToReplicator: Map; + deferredUpdates: Map; + + // PeerInfo + host?: PeerInfo; + local?: PeerInfo; + peers: PeerInfo[]; + peerIdCount: PeerID; + peerKeyToInfo: Map; + peerIdToInfo: Map; + // TODO: derive from Networked.authorId instead + entityIdToPeer: Map; + + // NetworkId + networkIdToEntityId: Map; + networkIdCount: NetworkID; + + // old localIdCount: number; removedLocalIds: number[]; + + // Messages messageHandlers: { [key: number]: (ctx: GameContext, v: CursorView, peerId: string) => void; }; cursorView: CursorView; - tickRate: number; - prefabToReplicator: Map; - deferredUpdates: Map; + incomingReliableRingBuffer: NetworkRingBuffer; + incomingUnreliableRingBuffer: NetworkRingBuffer; + outgoingReliableRingBuffer: NetworkRingBuffer; + outgoingUnreliableRingBuffer: NetworkRingBuffer; + // feature flags + tickRate: number; interpolate: boolean; } @@ -84,6 +109,10 @@ export interface GameNetworkState { * Initialization * *****************/ +const threadMessageQueue = createQueue>(); + +export const newPeersQueue = createQueue(); + export const NetworkModule = defineModule({ name: "network", create: async (ctx, { waitForMessage }): Promise => { @@ -95,55 +124,60 @@ export const NetworkModule = defineModule({ } = await waitForMessage(Thread.Main, NetworkMessageType.InitializeNetworkState); return { - onExitWorldQueue: [], - incomingReliableRingBuffer, - incomingUnreliableRingBuffer, - outgoingReliableRingBuffer, - outgoingUnreliableRingBuffer, - commands: [], - hostId: "", - peerId: "", + // NetworkReplicator + replicatorIdCount: 1, + replicators: new Map(), + prefabToReplicator: new Map(), + deferredUpdates: new Map(), + + // PeerInfo peers: [], - newPeers: [], - peerIdToIndex: new Map(), - peerIdToHistorian: new Map(), - networkIdToEntityId: new Map(), - peerIdToEntityId: new Map(), - peerIdToXRMode: new Map(), - entityIdToPeerId: new Map(), - indexToPeerId: new Map(), - peerIdCount: 0, + peerKeyToInfo: new Map(), + peerIdToInfo: new Map(), + entityIdToPeer: new Map(), + peerIdCount: 1n, + + // NetworkID + networkIdCount: 1n, localIdCount: 1, removedLocalIds: [], + networkIdToEntityId: new Map(), + + // Messages messageHandlers: {}, cursorView: createCursorView(), - prefabToReplicator: new Map(), - deferredUpdates: new Map(), - tickRate: 10, + incomingReliableRingBuffer, + incomingUnreliableRingBuffer, + outgoingReliableRingBuffer, + outgoingUnreliableRingBuffer, + + // feature flags + tickRate: 60, interpolate: false, }; }, init(ctx: GameContext) { const network = getModule(ctx, NetworkModule); - // TODO: make new API for this that allows user to use strings (internally mapped to an integer) - registerInboundMessageHandler(network, NetworkAction.Create, deserializeCreates); - registerInboundMessageHandler(network, NetworkAction.UpdateChanged, deserializeUpdatesChanged); - registerInboundMessageHandler(network, NetworkAction.UpdateSnapshot, deserializeUpdatesSnapshot); - registerInboundMessageHandler(network, NetworkAction.Delete, deserializeDeletes); - registerInboundMessageHandler(network, NetworkAction.FullSnapshot, deserializeSnapshot); - registerInboundMessageHandler(network, NetworkAction.FullChanged, deserializeFullChangedUpdate); - registerInboundMessageHandler(network, NetworkAction.UpdateNetworkId, deserializeUpdateNetworkId); - registerInboundMessageHandler(network, NetworkAction.InformPlayerNetworkId, deserializeInformPlayerNetworkId); - registerInboundMessageHandler(network, NetworkAction.NewPeerSnapshot, deserializeNewPeerSnapshot); - registerInboundMessageHandler(network, NetworkAction.RemoveOwnershipMessage, deserializeRemoveOwnership); - registerInboundMessageHandler(network, NetworkAction.InformXRMode, deserializeInformXRMode); + registerInboundMessageHandler(network, NetworkMessage.HostSnapshot, deserializeHostSnapshot); + registerInboundMessageHandler(network, NetworkMessage.HostCommands, deserializeHostCommands); + registerInboundMessageHandler(network, NetworkMessage.EntityUpdates, deserializeEntityUpdates); + registerInboundMessageHandler(network, NetworkMessage.PeerEntered, deserializePeerEntered); + registerInboundMessageHandler(network, NetworkMessage.PeerExited, deserializePeerExited); const disposables = [ - registerMessageHandler(ctx, NetworkMessageType.SetHost, onSetHost), - registerMessageHandler(ctx, NetworkMessageType.AddPeerId, onAddPeerId), - registerMessageHandler(ctx, NetworkMessageType.RemovePeerId, onRemovePeerId), - registerMessageHandler(ctx, ThirdRoomMessageType.ExitWorld, onExitWorld), + registerMessageHandler(ctx, NetworkMessageType.SetHost, (ctx: GameContext, message: SetHostMessage) => { + threadMessageQueue.enqueue(message); + }), + registerMessageHandler(ctx, NetworkMessageType.AddPeerId, (ctx: GameContext, message: AddPeerIdMessage) => { + threadMessageQueue.enqueue(message); + }), + registerMessageHandler(ctx, NetworkMessageType.RemovePeerId, (ctx: GameContext, message: RemovePeerIdMessage) => { + threadMessageQueue.enqueue(message); + }), + registerMessageHandler(ctx, ThirdRoomMessageType.ExitWorld, (ctx: GameContext, message: ExitWorldMessage) => { + threadMessageQueue.enqueue(message); + }), ]; return () => { @@ -158,148 +192,182 @@ export const NetworkModule = defineModule({ * Message Handlers * *******************/ -export const addPeerId = (ctx: GameContext, peerId: string) => { - console.info("addPeerId", peerId); +export const addPeerId = (ctx: GameContext, peerKey: string) => { + console.info("addPeerId", peerKey); const network = getModule(ctx, NetworkModule); - if (network.peers.includes(peerId) || network.peerId === peerId) return; - - network.peers.push(peerId); - network.newPeers.push(peerId); - - network.peerIdToHistorian.set(peerId, createHistorian()); - - mapPeerIdAndIndex(network, peerId); - - const peerIndex = network.peerIdToIndex.get(peerId) || 0; + if (network.peers.some((p) => p.key === peerKey) || network.local?.key === peerKey) { + return; + } - ctx.sendMessage(Thread.Game, { type: NetworkMessageType.PeerEntered, peerIndex }); + if (isHost(network)) { + const peerId = network.peerIdCount++; + const peerInfo = addPeerInfo(network, peerKey, peerId); + // queue peer up for avatar creation and host snapshot + newPeersQueue.enqueue(peerInfo); + ctx.sendMessage(Thread.Game, { type: NetworkMessageType.PeerEntered, peerIndex: peerId }); + } else { + waitUntil(() => network.local?.id).then((peerId) => { + ctx.sendMessage(Thread.Game, { type: NetworkMessageType.PeerEntered, peerIndex: peerId }); + }); + } }; const onAddPeerId = (ctx: GameContext, message: AddPeerIdMessage) => addPeerId(ctx, message.peerId); -export const removePeerId = (ctx: GameContext, peerId: string) => { +export const removePeerId = (ctx: GameContext, peerKey: string) => { const network = getModule(ctx, NetworkModule); + if (!isHost(network)) { + return; + } - const peerArrIndex = network.peers.indexOf(peerId); - const peerIndex = network.peerIdToIndex.get(peerId); + const peerArrIndex = network.peers.findIndex((p) => p.key === peerKey); + const peerId = network.peers[peerArrIndex]?.id; - if (peerArrIndex > -1 && peerIndex) { + if (peerArrIndex > -1 && peerId) { const entities = networkedQuery(ctx.world); for (let i = entities.length - 1; i >= 0; i--) { const eid = entities[i]; - const node = getRemoteResource(ctx, eid); - const networkId = Networked.networkId[eid]; + // skip if the entity does not belong to this peer + if (Networked.authorId[eid] !== Number(peerId)) { + continue; + } + + const node = tryGetRemoteResource(ctx, eid); + + const hosting = isHost(network); + const destroyOnLeave = Networked.destroyOnLeave[eid]; - // if the entity's networkId contains the peerIndex it means that peer owns the entity - if (node && peerIndex === getPeerIndexFromNetworkId(networkId)) { - network.entityIdToPeerId.delete(eid); - removeObjectFromWorld(ctx, node); + if (hosting && destroyOnLeave) { + const replicator = tryGetNetworkReplicator(network, Networked.replicatorId[eid]); + + replicator.despawn(node); } } - network.peers.splice(peerArrIndex, 1); + removePeerInfo(network, peerKey); - ctx.sendMessage(Thread.Game, { type: NetworkMessageType.PeerExited, peerIndex }); + ctx.sendMessage(Thread.Game, { type: NetworkMessageType.PeerExited, peerIndex: peerId }); } else { - console.warn(`cannot remove peerId ${peerId}, does not exist in peer list`); + console.warn(`cannot remove peerId ${peerKey}, does not exist in peer list`); } }; const onRemovePeerId = (ctx: GameContext, message: RemovePeerIdMessage) => removePeerId(ctx, message.peerId); -const onExitWorld = (ctx: GameContext, message: ExitWorldMessage) => { +const onExitWorld = (ctx: GameContext) => { const network = getModule(ctx, NetworkModule); - network.onExitWorldQueue.push(message); -}; - -export function NetworkExitWorldQueueSystem(ctx: GameContext) { - const network = getModule(ctx, NetworkModule); - - while (network.onExitWorldQueue.length) { - network.onExitWorldQueue.shift(); - - network.hostId = ""; - network.peers = []; - network.newPeers = []; - network.peerIdToEntityId.clear(); - network.entityIdToPeerId.clear(); - network.networkIdToEntityId.clear(); - network.localIdCount = 1; - network.removedLocalIds = []; - network.commands = []; - // drain ring buffers - const ringOut = { packet: new ArrayBuffer(0), peerId: "", broadcast: false }; - while (availableRead(network.outgoingReliableRingBuffer)) - dequeueNetworkRingBuffer(network.outgoingReliableRingBuffer, ringOut); - while (availableRead(network.incomingReliableRingBuffer)) - dequeueNetworkRingBuffer(network.incomingReliableRingBuffer, ringOut); - } -} -// Set local peer id -export const setLocalPeerId = (ctx: GameContext, localPeerId: string) => { - const network = getModule(ctx, NetworkModule); - network.peerId = localPeerId; - mapPeerIdAndIndex(network, localPeerId); + // PeerInfo + network.host = undefined; + network.local = undefined; + network.peers = []; + network.entityIdToPeer.clear(); + network.networkIdToEntityId.clear(); + network.peerKeyToInfo.clear(); + network.peerIdToInfo.clear(); + network.peerIdCount = 1n; + + // NetworkID + network.networkIdCount = 1n; + + // TODO: remove + network.localIdCount = 1; + network.removedLocalIds = []; + + // drain queues + const ringOut = { packet: new ArrayBuffer(0), peerKey: "", broadcast: false }; + while (availableRead(network.outgoingReliableRingBuffer)) + dequeueNetworkRingBuffer(network.outgoingReliableRingBuffer, ringOut); + while (availableRead(network.incomingReliableRingBuffer)) + dequeueNetworkRingBuffer(network.incomingReliableRingBuffer, ringOut); + + while (threadMessageQueue.length) threadMessageQueue.dequeue(); + while (newPeersQueue.length) newPeersQueue.dequeue(); }; const onSetHost = async (ctx: GameContext, message: SetHostMessage) => { const network = getModule(ctx, NetworkModule); - const newHostId = message.hostId; - network.hostId = newHostId; -}; + const hostKey = message.hostId; -/* Utils */ + const hostInfo = network.peers.find((p) => p.key === hostKey); -const mapPeerIdAndIndex = (network: GameNetworkState, peerId: string) => { - const peerIndex = murmurHash(peerId) >>> 16; - network.peerIdToIndex.set(peerId, peerIndex); - network.indexToPeerId.set(peerIndex, peerId); + if (hostInfo) { + network.host = hostInfo; + } else { + const id = isHost(network) ? network.host?.id : undefined; + network.host = { + id, + key: hostKey, + lastUpdate: 0, + xrMode: XRMode.None, + }; + } }; -const isolateBits = (val: number, n: number, offset = 0) => val & (((1 << n) - 1) << offset); +/* Utils */ -export const getPeerIndexFromNetworkId = (nid: number) => isolateBits(nid, 16); -export const getLocalIdFromNetworkId = (nid: number) => isolateBits(nid >>> 16, 16); +export const addPeerInfo = (network: GameNetworkState, peerKey: string, peerId: PeerID) => { + if (network.peers.some((p) => p.key === peerKey)) { + throw new Error("PeerInfo already exists for peerKey " + peerKey); + } -export const setPeerIdIndexInNetworkId = (nid: number, peerIdIndex: number) => { - const localId = getLocalIdFromNetworkId(nid); - return ((localId << 16) | peerIdIndex) >>> 0; -}; + const peerInfo: PeerInfo & { id: PeerID } = { + id: peerId, + key: peerKey, + // TODO + xrMode: XRMode.None, + lastUpdate: 0, + }; -export const createNetworkId = (ctx: GameContext) => { - const network = getModule(ctx, NetworkModule); + network.peerIdToInfo.set(peerId, peerInfo); + network.peerKeyToInfo.set(peerKey, peerInfo); + network.peers.push(peerInfo); - const localId = network.removedLocalIds.shift() || network.localIdCount++; - const peerIndex = network.peerIdToIndex.get(network.peerId); + return peerInfo; +}; - if (peerIndex === undefined) { - throw new Error("could not create networkId, peerId not set in peerIdToIndex map"); +export const removePeerInfo = (network: GameNetworkState, peerKey: string) => { + const i = network.peers.findIndex((p) => p.key === peerKey); + if (i === -1) { + throw new Error("PeerInfo does not exist for peerKey " + peerKey); } - console.info("createNetworkId - localId:", localId, "; peerIndex:", peerIndex); + const peerInfo = network.peers[i]; - // bitwise operations in JS are limited to 32 bit integers (https://developer.mozilla.org/en-US/docs/web/javascript/reference/operators#binary_bitwise_operators) - // logical right shift by 0 to treat as an unsigned integer - return ((localId << 16) | peerIndex) >>> 0; + if (peerInfo.entityId) network.entityIdToPeer.delete(peerInfo.entityId); + if (peerInfo.id) network.peerIdToInfo.delete(peerInfo.id); + network.peerKeyToInfo.delete(peerInfo.key); + network.peers.splice(i, 1); }; -export const removeNetworkId = (ctx: GameContext, nid: number) => { - const network = getModule(ctx, NetworkModule); - const localId = getLocalIdFromNetworkId(nid); - if (network.removedLocalIds.includes(localId)) { - console.warn(`could not remove localId ${localId}, already removed`); - } else { - network.removedLocalIds.push(localId); +export const tryGetPeerId = (network: GameNetworkState, peerKey: string) => { + const peerInfo = network.peerKeyToInfo.get(peerKey); + if (!peerInfo || !peerInfo.id) { + throw new Error("PeerId not found for peerKey: " + peerKey); } + return peerInfo.id; }; -export const associatePeerWithEntity = (network: GameNetworkState, peerId: string, eid: number) => { - network.peerIdToEntityId.set(peerId, eid); - network.entityIdToPeerId.set(eid, peerId); +export const getPeerId = (network: GameNetworkState, peerKey: string) => network.peerKeyToInfo.get(peerKey)?.id; +export const getPeerKey = (network: GameNetworkState, peerId: PeerID) => network.peerIdToInfo.get(peerId)?.key; + +function _throw(m: string): PeerInfo { + throw new Error(m); +} + +export const getPeerInfoById = (network: GameNetworkState, peerId: PeerID) => network.peerIdToInfo.get(peerId); +export const tryGetPeerInfoById = (network: GameNetworkState, peerId: PeerID) => + network.peerIdToInfo.get(peerId) || _throw("PeerInfo not found for peerId: " + peerId); + +export const getPeerInfoByKey = (network: GameNetworkState, peerKey: string) => network.peerKeyToInfo.get(peerKey); +export const tryGetPeerInfoByKey = (network: GameNetworkState, peerKey: string): PeerInfo => + network.peerKeyToInfo.get(peerKey) || _throw("PeerInfo not found for peerKey: " + peerKey); + +export const createNetworkId = (network: GameNetworkState) => { + return network.networkIdCount++; }; /* Queries */ @@ -307,21 +375,44 @@ export const associatePeerWithEntity = (network: GameNetworkState, peerId: strin export const networkedQuery = defineQuery([Networked]); export const exitedNetworkedQuery = exitQuery(networkedQuery); -export const ownedNetworkedQuery = defineQuery([Networked, Owned]); -export const createdOwnedNetworkedQuery = enterQuery(ownedNetworkedQuery); -export const deletedOwnedNetworkedQuery = exitQuery(ownedNetworkedQuery); +export const relayingNetworkedQuery = defineQuery([Networked, Relaying]); + +export const authoringNetworkedQuery = defineQuery([Networked, Authoring]); +export const spawnedNetworkeQuery = enterQuery(authoringNetworkedQuery); +export const despawnedNetworkQuery = exitQuery(authoringNetworkedQuery); -export const remoteNetworkedQuery = defineQuery([Networked, Not(Owned)]); +export const remoteNetworkedQuery = defineQuery([Networked, Not(Authoring)]); -// bitecs todo: add defineQueue to bitECS / allow multiple enter/exit queries to avoid duplicate query -export const networkIdQuery = defineQuery([Networked, Owned]); -export const enteredNetworkIdQuery = enterQuery(networkIdQuery); -export const exitedNetworkIdQuery = exitQuery(networkIdQuery); +export const ownedPlayerQuery = defineQuery([OurPlayer, Authoring]); -export const ownedPlayerQuery = defineQuery([Player, Owned]); -export const enteredOwnedPlayerQuery = enterQuery(ownedPlayerQuery); -export const exitedOwnedPlayerQuery = exitQuery(ownedPlayerQuery); +const MessageTypeHandler: { [key: string]: Function } = { + [NetworkMessageType.SetHost]: onSetHost, + [NetworkMessageType.AddPeerId]: onAddPeerId, + [NetworkMessageType.RemovePeerId]: onRemovePeerId, + [ThirdRoomMessageType.ExitWorld]: onExitWorld, +}; + +export function HostSpawnPeerAvatarSystem(ctx: GameContext) { + const thirdroom = getModule(ctx, ThirdRoomModule); + const network = getModule(ctx, NetworkModule); + + if (!isHost(network)) { + return; + } + + // don't drain the queue, it is later drained by the OutboundNetworkSystem + for (const peerInfo of newPeersQueue) { + const avatar = thirdroom.replicators!.avatar.spawn(ctx); -// export const remotePlayerQuery = defineQuery([Player, Not(Owned)]); -// export const enteredRemotePlayerQuery = enterQuery(remotePlayerQuery); -// export const exitedRemotePlayerQuery = exitQuery(remotePlayerQuery); + addComponent(ctx.world, Relaying, avatar.eid); + Relaying.for[avatar.eid] = Number(peerInfo.id); + Networked.authorId[avatar.eid] = Number(peerInfo.id); + } +} + +export function NetworkThreadedMessageQueueSystem(ctx: GameContext) { + let message; + while ((message = threadMessageQueue.dequeue())) { + MessageTypeHandler[message.type](ctx, message); + } +} diff --git a/src/engine/network/network.main.ts b/src/engine/network/network.main.ts index 82fe84359..1ce0e8b3b 100644 --- a/src/engine/network/network.main.ts +++ b/src/engine/network/network.main.ts @@ -9,9 +9,9 @@ import { dequeueNetworkRingBuffer, enqueueNetworkRingBuffer, NetworkRingBuffer, -} from "./RingBuffer"; +} from "./NetworkRingBuffer"; import { createCursorView, readUint8 } from "../allocator/CursorView"; -import { UnreliableNetworkActions } from "./NetworkAction"; +import { UnreliableNetworkActions } from "./NetworkMessage"; /********* * Types * @@ -26,8 +26,8 @@ export interface MainNetworkState { incomingUnreliableRingBuffer: NetworkRingBuffer; outgoingReliableRingBuffer: NetworkRingBuffer; outgoingUnreliableRingBuffer: NetworkRingBuffer; - peerId?: string; - hostId?: string; + peerKey?: string; + hostKey?: string; } /****************** @@ -116,16 +116,16 @@ function onPeerLeft(ctx: MainContext, peerId: string) { * API * ******/ -export function setHost(ctx: MainContext, hostId: string) { +export function setHost(ctx: MainContext, hostKey: string) { const network = getModule(ctx, NetworkModule); - const hostChanged = network.hostId !== hostId; + const hostChanged = network.hostKey !== hostKey; if (hostChanged) { - console.info("electing new host", hostId); - network.hostId = hostId; + console.info("electing new host", hostKey); + network.hostKey = hostKey; ctx.sendMessage(Thread.Game, { type: NetworkMessageType.SetHost, - hostId, + hostId: hostKey, }); } } @@ -136,32 +136,34 @@ export function hasPeer(ctx: MainContext, peerId: string): boolean { return reliableChannels.has(peerId); } -export function addPeer(ctx: MainContext, peerId: string, dataChannel: RTCDataChannel, mediaStream?: MediaStream) { +export function addPeer(ctx: MainContext, peerKey: string, dataChannel: RTCDataChannel, mediaStream?: MediaStream) { const network = getModule(ctx, NetworkModule); const audio = getModule(ctx, AudioModule); const { reliableChannels, unreliableChannels } = network; - if (reliableChannels.has(peerId)) { - console.warn("peer already added", peerId); + if (reliableChannels.has(peerKey)) { + console.warn("peer already added", peerKey); return; } - if (dataChannel.ordered) reliableChannels.set(peerId, dataChannel); - else unreliableChannels.set(peerId, dataChannel); + if (dataChannel.ordered) reliableChannels.set(peerKey, dataChannel); + else unreliableChannels.set(peerKey, dataChannel); const onOpen = () => { const onClose = () => { - onPeerLeft(ctx, peerId); + onPeerLeft(ctx, peerKey); }; - const handler = onIncomingMessage(ctx, network, peerId); - network.incomingMessageHandlers.set(peerId, handler); + const handler = onIncomingMessage(ctx, network, peerKey); + network.incomingMessageHandlers.set(peerKey, handler); dataChannel.addEventListener("message", handler); dataChannel.addEventListener("close", onClose); + network.peerKey = peerKey; + ctx.sendMessage(Thread.Game, { type: NetworkMessageType.AddPeerId, - peerId, + peerId: peerKey, }); }; @@ -174,17 +176,17 @@ export function addPeer(ctx: MainContext, peerId: string, dataChannel: RTCDataCh } if (mediaStream) { - setPeerMediaStream(audio, peerId, mediaStream); + setPeerMediaStream(audio, peerKey, mediaStream); } } -export function removePeer(ctx: MainContext, peerId: string) { - onPeerLeft(ctx, peerId); +export function removePeer(ctx: MainContext, peerKey: string) { + onPeerLeft(ctx, peerKey); } -export function toggleMutePeer(ctx: MainContext, peerId: string) { +export function toggleMutePeer(ctx: MainContext, peerKey: string) { const audio = getModule(ctx, AudioModule); - const mediaStream = audio.mediaStreams.get(peerId); + const mediaStream = audio.mediaStreams.get(peerKey); if (mediaStream) { const tracks = mediaStream.getAudioTracks(); if (tracks[0].enabled) tracks.forEach((t) => (t.enabled = false)); @@ -209,7 +211,7 @@ export function disconnect(ctx: MainContext) { } } -const ringOut = { packet: new ArrayBuffer(0), peerId: "", broadcast: false }; +const ringOut = { packet: new ArrayBuffer(0), peerKey: "", broadcast: false }; export function MainThreadNetworkSystem(ctx: MainContext) { const network = getModule(ctx, NetworkModule); @@ -225,9 +227,9 @@ export function MainThreadNetworkSystem(ctx: MainContext) { peer.send(ringOut.packet); }); } else { - const peer = network.reliableChannels.get(ringOut.peerId); + const peer = network.reliableChannels.get(ringOut.peerKey); if (!peer) { - console.error("Failed to send message, peer's reliable channel not found", ringOut.peerId); + console.error("Failed to send message, peer's reliable channel not found", ringOut.peerKey); continue; } @@ -248,9 +250,9 @@ export function MainThreadNetworkSystem(ctx: MainContext) { peer.send(ringOut.packet); }); } else { - const peer = network.reliableChannels.get(ringOut.peerId); + const peer = network.reliableChannels.get(ringOut.peerKey); if (!peer) { - console.error("peer's unreliable channel is not found", ringOut.peerId); + console.error("peer's unreliable channel is not found", ringOut.peerKey); continue; } diff --git a/src/engine/network/outbound.game.ts b/src/engine/network/outbound.game.ts deleted file mode 100644 index 4c1b05a18..000000000 --- a/src/engine/network/outbound.game.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { NOOP } from "../config.common"; -import { GameContext } from "../GameTypes"; -import { getModule } from "../module/module.common"; -import { getXRMode } from "../renderer/renderer.game"; -import { - NetworkModule, - enteredNetworkIdQuery, - createNetworkId, - exitedNetworkIdQuery, - removeNetworkId, - exitedNetworkedQuery, - ownedPlayerQuery, - GameNetworkState, -} from "./network.game"; -import { Networked } from "./NetworkComponents"; -import { enqueueNetworkRingBuffer } from "./RingBuffer"; -import { - createNewPeerSnapshotMessage, - createInformPlayerNetworkIdMessage, - createCreateMessage, - createDeleteMessage, - createUpdateChangedMessage, - createInformXRModeMessage, -} from "./serialization.game"; - -export const broadcastReliable = (ctx: GameContext, network: GameNetworkState, packet: ArrayBuffer) => { - if (!packet.byteLength) return; - if (!enqueueNetworkRingBuffer(network.outgoingReliableRingBuffer, "", packet, true)) { - console.warn("outgoing reliable network ring buffer full"); - } -}; - -export const broadcastUnreliable = (ctx: GameContext, network: GameNetworkState, packet: ArrayBuffer) => { - if (!packet.byteLength) return; - if (!enqueueNetworkRingBuffer(network.outgoingUnreliableRingBuffer, "", packet, true)) { - console.warn("outgoing unreliable network ring buffer full"); - } -}; - -export const sendReliable = (ctx: GameContext, network: GameNetworkState, peerId: string, packet: ArrayBuffer) => { - if (!packet.byteLength) return; - if (!enqueueNetworkRingBuffer(network.outgoingReliableRingBuffer, peerId, packet)) { - console.warn("outgoing reliable network ring buffer full"); - } -}; - -export const sendUnreliable = (ctx: GameContext, network: GameNetworkState, peerId: string, packet: ArrayBuffer) => { - if (!packet.byteLength) return; - if (!enqueueNetworkRingBuffer(network.outgoingUnreliableRingBuffer, peerId, packet)) { - console.warn("outgoing unreliable network ring buffer full"); - } -}; - -const assignNetworkIds = (ctx: GameContext) => { - const network = getModule(ctx, NetworkModule); - const entered = enteredNetworkIdQuery(ctx.world); - for (let i = 0; i < entered.length; i++) { - const eid = entered[i]; - const nid = createNetworkId(ctx) || 0; - - Networked.networkId[eid] = nid; - console.info("networkId", nid, "assigned to eid", eid); - network.networkIdToEntityId.set(nid, eid); - } - return ctx; -}; - -const unassignNetworkIds = (ctx: GameContext) => { - const exited = exitedNetworkIdQuery(ctx.world); - for (let i = 0; i < exited.length; i++) { - const eid = exited[i]; - console.info("networkId", Networked.networkId[eid], "deleted from eid", eid); - removeNetworkId(ctx, Networked.networkId[eid]); - Networked.networkId[eid] = NOOP; - } - return ctx; -}; - -function disposeNetworkedEntities(ctx: GameContext) { - const network = getModule(ctx, NetworkModule); - const exited = exitedNetworkedQuery(ctx.world); - - for (let i = 0; i < exited.length; i++) { - const eid = exited[i]; - network.networkIdToEntityId.delete(Networked.networkId[eid]); - } -} - -const sendUpdatesPeerToPeer = (ctx: GameContext) => { - const network = getModule(ctx, NetworkModule); - - // only send updates when: - // - we have connected peers - // - peerIdIndex has been assigned - // - player rig has spawned - const haveConnectedPeers = network.peers.length > 0; - const spawnedPlayerRig = ownedPlayerQuery(ctx.world).length > 0; - - if (haveConnectedPeers && spawnedPlayerRig) { - // send snapshot update to all new peers - const haveNewPeers = network.newPeers.length > 0; - if (haveNewPeers) { - const newPeerSnapshotMsg = createNewPeerSnapshotMessage(ctx, network.cursorView); - - while (network.newPeers.length) { - const theirPeerId = network.newPeers.shift(); - if (theirPeerId) { - // send out the snapshot first so entities that the next messages may reference exist - sendReliable(ctx, network, theirPeerId, newPeerSnapshotMsg); - - // inform new peer of our avatar's networkId - sendReliable(ctx, network, theirPeerId, createInformPlayerNetworkIdMessage(ctx, network.peerId)); - - // inform other clients of our XRMode - broadcastReliable(ctx, network, createInformXRModeMessage(ctx, getXRMode(ctx))); - } - } - } else { - // send reliable creates - const createMsg = createCreateMessage(ctx, network.cursorView); - broadcastReliable(ctx, network, createMsg); - - // send reliable deletes - const deleteMsg = createDeleteMessage(ctx, network.cursorView); - broadcastReliable(ctx, network, deleteMsg); - - // send unreliable updates - const updateMsg = createUpdateChangedMessage(ctx, network.cursorView); - broadcastUnreliable(ctx, network, updateMsg); - } - } - - return ctx; -}; - -export function OutboundNetworkSystem(ctx: GameContext) { - const network = getModule(ctx, NetworkModule); - - const hasPeerIdIndex = network.peerIdToIndex.has(network.peerId); - if (!hasPeerIdIndex) return ctx; - - // assign networkIds before serializing game state - assignNetworkIds(ctx); - - // serialize and send all outgoing updates - try { - sendUpdatesPeerToPeer(ctx); - } catch (e) { - console.error(e); - } - - // delete networkIds after serializing game state (deletes serialization needs to know the nid before removal) - unassignNetworkIds(ctx); - - disposeNetworkedEntities(ctx); - - return ctx; -} diff --git a/src/engine/network/ownership.game.ts b/src/engine/network/ownership.game.ts index f79ac328e..a59c1e391 100644 --- a/src/engine/network/ownership.game.ts +++ b/src/engine/network/ownership.game.ts @@ -1,6 +1,6 @@ import { addComponent, hasComponent } from "bitecs"; -import { sliceCursorView, CursorView, writeUint32, readUint32, createCursorView } from "../allocator/CursorView"; +import { sliceCursorView, CursorView, createCursorView, readUint64, writeUint64 } from "../allocator/CursorView"; import { NOOP } from "../config.common"; import { GameContext } from "../GameTypes"; import { getModule } from "../module/module.common"; @@ -8,28 +8,28 @@ import { getPrefabTemplate, Prefab } from "../prefab/prefab.game"; import { getRemoteResource } from "../resource/resource.game"; import { addObjectToWorld, RemoteNode, removeObjectFromWorld } from "../resource/RemoteResources"; import { GameNetworkState, NetworkModule } from "./network.game"; -import { Networked, Owned } from "./NetworkComponents"; -import { NetworkAction } from "./NetworkAction"; -import { broadcastReliable } from "./outbound.game"; -import { writeMetadata } from "./serialization.game"; +import { Networked, Authoring } from "./NetworkComponents"; +import { NetworkMessage } from "./NetworkMessage"; +import { writeMessageType } from "./serialization.game"; import { applyTransformToRigidBody } from "../physics/physics.game"; +import { enqueueReliableBroadcast } from "./NetworkRingBuffer"; // const messageView = createCursorView(new ArrayBuffer(Uint32Array.BYTES_PER_ELEMENT * 3)); const messageView = createCursorView(new ArrayBuffer(Uint32Array.BYTES_PER_ELEMENT * 30)); export const createRemoveOwnershipMessage = (ctx: GameContext, eid: number) => { - writeMetadata(messageView, NetworkAction.RemoveOwnershipMessage); + writeMessageType(messageView, NetworkMessage.RemoveOwnershipMessage); serializeRemoveOwnership(messageView, eid); return sliceCursorView(messageView); }; export const serializeRemoveOwnership = (cv: CursorView, eid: number) => { - writeUint32(cv, Networked.networkId[eid]); + writeUint64(cv, BigInt(Networked.networkId[eid])); }; export const deserializeRemoveOwnership = (ctx: GameContext, cv: CursorView) => { const network = getModule(ctx, NetworkModule); - const nid = readUint32(cv); + const nid = readUint64(cv); const eid = network.networkIdToEntityId.get(nid); const node = eid ? getRemoteResource(ctx, eid) : undefined; if (node) { @@ -42,12 +42,12 @@ export const takeOwnership = (ctx: GameContext, network: GameNetworkState, oldNo if ( hasComponent(ctx.world, Prefab, eid) && hasComponent(ctx.world, Networked, eid) && - !hasComponent(ctx.world, Owned, eid) + !hasComponent(ctx.world, Authoring, eid) ) { removeObjectFromWorld(ctx, oldNode); // send message to remove on other side - broadcastReliable(ctx, network, createRemoveOwnershipMessage(ctx, oldNode.eid)); + enqueueReliableBroadcast(network, createRemoveOwnershipMessage(ctx, oldNode.eid)); const prefabName = Prefab.get(eid); if (!prefabName) throw new Error("could not take ownership, prefab name not found: " + prefabName); @@ -55,7 +55,7 @@ export const takeOwnership = (ctx: GameContext, network: GameNetworkState, oldNo const template = getPrefabTemplate(ctx, prefabName); const newNode = template.create(ctx); - addComponent(ctx.world, Owned, newNode.eid); + addComponent(ctx.world, Authoring, newNode.eid); addComponent(ctx.world, Networked, newNode.eid); const body = newNode.physicsBody?.body; diff --git a/src/engine/network/scripting.game.ts b/src/engine/network/scripting.game.ts index e7bf5d1f4..14dc91718 100644 --- a/src/engine/network/scripting.game.ts +++ b/src/engine/network/scripting.game.ts @@ -8,15 +8,16 @@ import { writeArrayBuffer as cursorWriteArrayBuffer, writeInt32, CursorView, + writeUint64, + readUint64, } from "../allocator/CursorView"; import { GameContext } from "../GameTypes"; import { defineModule, getModule, registerMessageHandler } from "../module/module.common"; -import { GameNetworkState, NetworkModule } from "./network.game"; -import { NetworkAction } from "./NetworkAction"; -import { broadcastReliable, sendReliable, sendUnreliable } from "./outbound.game"; -import { writeMetadata } from "./serialization.game"; +import { GameNetworkState, NetworkModule, PeerID, getPeerId, tryGetPeerInfoById } from "./network.game"; +import { NetworkMessage } from "./NetworkMessage"; +import { writeMessageType } from "./serialization.game"; import { writeUint32, readUint32 } from "../allocator/CursorView"; -import { registerInboundMessageHandler } from "./inbound.game"; +import { registerInboundMessageHandler } from "./InboundNetworkSystem"; import { getScriptResource, readExtensionsAndExtras, @@ -32,8 +33,9 @@ import { createDisposables } from "../utils/createDisposables"; import { NetworkMessageType, PeerEnteredMessage, PeerExitedMessage } from "./network.common"; import { ScriptComponent, scriptQuery } from "../scripting/scripting.game"; import { Replication, createReplicator } from "./Replicator"; -import { Networked, Owned } from "./NetworkComponents"; +import { Networked, Authoring } from "./NetworkComponents"; import { addPrefabComponent } from "../prefab/prefab.game"; +import { enqueueReliableBroadcast, enqueueReliable, enqueueUnreliable } from "./NetworkRingBuffer"; export const WebSGNetworkModule = defineModule({ name: "WebSGNetwork", @@ -42,10 +44,10 @@ export const WebSGNetworkModule = defineModule({ }, init(ctx: GameContext) { const network = getModule(ctx, NetworkModule); - registerInboundMessageHandler(network, NetworkAction.BinaryScriptMessage, (ctx, v, peerId) => + registerInboundMessageHandler(network, NetworkMessage.BinaryScriptMessage, (ctx, v, peerId) => deserializeScriptMessage(ctx, v, peerId, true) ); - registerInboundMessageHandler(network, NetworkAction.StringScriptMessage, (ctx, v, peerId) => + registerInboundMessageHandler(network, NetworkMessage.StringScriptMessage, (ctx, v, peerId) => deserializeScriptMessage(ctx, v, peerId, true) ); @@ -79,22 +81,22 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo const networkWASMModule = { network_get_host_peer_index() { - const peerIndex = network.peerIdToIndex.get(network.hostId); + const peerId = network.host?.id; - if (peerIndex === undefined) { - return 0; + if (peerId === undefined) { + return 0n; } - return peerIndex; + return peerId; }, network_get_local_peer_index() { - const peerIndex = network.peerIdToIndex.get(network.peerId); + const peerId = network.local?.id; - if (peerIndex === undefined) { - return 0; + if (peerId === undefined) { + return 0n; } - return peerIndex; + return peerId; }, network_broadcast: (packetPtr: number, byteLength: number, binary: number, reliable: number) => { try { @@ -103,7 +105,7 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo const msg = createScriptMessage(ctx, scriptPacket, !!binary); if (reliable) { - broadcastReliable(ctx, network, msg); + enqueueReliableBroadcast(network, msg); return 0; } else { console.error("WebSGNetworking: Unreliable broadcast currently not supported."); @@ -142,7 +144,7 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo if (!listener) { moveCursorView(wasmCtx.cursorView, infoPtr); - writeUint32(wasmCtx.cursorView, 0); + writeUint64(wasmCtx.cursorView, 0n); writeUint32(wasmCtx.cursorView, 0); writeInt32(wasmCtx.cursorView, 0); console.error(`WebSGNetworking: Listener ${listenerId} does not exist.`); @@ -150,12 +152,12 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo } let message: [string, ArrayBuffer, boolean] | undefined; - let peerIndex: number | undefined; + let peerIndex: PeerID | undefined; while (listener.inbound.length > 0) { message = listener.inbound[0]; const peerId = message[0]; - peerIndex = network.peerIdToIndex.get(peerId); + peerIndex = getPeerId(network, peerId); if (peerIndex === undefined) { // This message is from a peer that no longer exists. @@ -168,7 +170,7 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo } moveCursorView(wasmCtx.cursorView, infoPtr); - writeUint32(wasmCtx.cursorView, peerIndex || 0); + writeUint64(wasmCtx.cursorView, peerIndex || 0n); writeUint32(wasmCtx.cursorView, message ? message[1].byteLength : 0); writeInt32(wasmCtx.cursorView, message ? (message[2] ? 1 : 0) : 0); @@ -193,7 +195,7 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo } const peerId = message[0]; - const peerIndex = network.peerIdToIndex.get(peerId); + const peerIndex = getPeerId(network, peerId); if (peerIndex === undefined) { console.warn("Discarded message from peer that no longer exists"); @@ -221,32 +223,34 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo return -1; } }, - peer_get_id_length(peerIndex: number) { - const peerId = network.indexToPeerId.get(peerIndex); + peer_get_id_length(peerId: PeerID) { + const peerInfo = tryGetPeerInfoById(network, peerId); + const peerKey = peerInfo.key; - if (!peerId) { - console.error(`WebSGNetworking: Peer ${peerIndex} does not exist.`); + if (!peerKey) { + console.error(`WebSGNetworking: PeerId ${peerId} does not exist.`); return -1; } - return peerId.length; + return peerKey.length; }, - peer_get_id(peerIndex: number, idPtr: number, maxBufLength: number) { - const peerId = network.indexToPeerId.get(peerIndex); + peer_get_id(peerId: PeerID, idPtr: number, maxBufLength: number) { + const peerInfo = tryGetPeerInfoById(network, peerId); + const peerKey = peerInfo.key; - if (!peerId) { - console.error(`WebSGNetworking: Peer ${peerIndex} does not exist.`); + if (!peerKey) { + console.error(`WebSGNetworking: Peer ${peerId} does not exist.`); return -1; } try { - return writeString(wasmCtx, idPtr, peerId, maxBufLength); + return writeString(wasmCtx, idPtr, peerKey, maxBufLength); } catch (error) { console.error(error); return -1; } }, - peer_get_translation_element(peerIndex: number, index: number) { + peer_get_translation_element(peerIndex: PeerID, index: number) { const node = getPeerNode(ctx, network, peerIndex); if (!node) { @@ -256,7 +260,7 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo return node.position[index]; }, - peer_get_translation(peerIndex: number, translationPtr: number) { + peer_get_translation(peerIndex: PeerID, translationPtr: number) { const node = getPeerNode(ctx, network, peerIndex); if (!node) { @@ -268,7 +272,7 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo return 0; }, - peer_get_rotation_element(peerIndex: number, index: number) { + peer_get_rotation_element(peerIndex: PeerID, index: number) { const node = getPeerNode(ctx, network, peerIndex); if (!node) { @@ -278,7 +282,7 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo return node.quaternion[index]; }, - peer_get_rotation(peerIndex: number, rotationPtr: number) { + peer_get_rotation(peerIndex: PeerID, rotationPtr: number) { const node = getPeerNode(ctx, network, peerIndex); if (!node) { @@ -290,32 +294,21 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo return 0; }, - peer_is_host(peerIndex: number) { - const peerId = network.indexToPeerId.get(peerIndex); - - if (!peerId) { - console.error(`WebSGNetworking: Peer index ${peerIndex} does not exist.`); - return -1; - } - - return network.hostId === peerId ? 1 : 0; + peer_is_host(peerId: PeerID) { + const peerInfo = tryGetPeerInfoById(network, peerId); + return network.host === peerInfo ? 1 : 0; }, - peer_is_local(peerIndex: number) { - const peerId = network.indexToPeerId.get(peerIndex); - - if (!peerId) { - console.error(`WebSGNetworking: Peer index ${peerIndex} does not exist.`); - return -1; - } - - return network.peerId === peerId ? 1 : 0; + peer_is_local(peerId: PeerID) { + const peerInfo = tryGetPeerInfoById(network, peerId); + return network.local === peerInfo ? 1 : 0; }, - peer_send: (peerIndex: number, packetPtr: number, byteLength: number, binary: number, reliable: number) => { + peer_send: (peerId: PeerID, packetPtr: number, byteLength: number, binary: number, reliable: number) => { try { - const peerId = network.indexToPeerId.get(peerIndex); + const peerInfo = tryGetPeerInfoById(network, peerId); + const peerKey = peerInfo.key; - if (!peerId) { - console.error(`WebSGNetworking: Peer ${peerIndex} does not exist.`); + if (!peerKey) { + console.error(`WebSGNetworking: PeerId ${peerId} does not exist.`); return -1; } @@ -324,10 +317,10 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo const msg = createScriptMessage(ctx, scriptPacket, !!binary); if (reliable) { - sendReliable(ctx, network, peerId, msg); + enqueueReliable(network, peerKey, msg); return 0; } else { - sendUnreliable(ctx, network, peerId, msg); + enqueueUnreliable(network, peerKey, msg); } } catch (error) { console.error("WebSGNetworking: Error broadcasting packet:", error); @@ -368,18 +361,16 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo addPrefabComponent(ctx.world, nodeId, replicator.prefabName); addComponent(ctx.world, Networked, nodeId); - addComponent(ctx.world, Owned, nodeId); + addComponent(ctx.world, Authoring, nodeId); const buffer = new Uint8Array([...readUint8Array(wasmCtx, packetPtr, byteLength)]); const data = byteLength > 0 ? buffer : undefined; - const peerId = network.peerId; - const peerIndex = network.peerIdToIndex.get(peerId)!; if (data) { replicator.eidToData.set(nodeId, data); } - replicator.spawned.push({ nodeId, peerIndex, data }); + replicator.spawned.push({ nodeId, peerIndex: network.local!.id!, data }); return 0; }, @@ -399,14 +390,12 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo const buffer = new Uint8Array([...readUint8Array(wasmCtx, packetPtr, byteLength)]); const data = byteLength > 0 ? buffer : undefined; - const peerId = network.peerId; - const peerIndex = network.peerIdToIndex.get(peerId)!; if (data) { replicator.eidToData.set(nodeId, data); } - replicator.despawned.push({ nodeId, peerIndex, data }); + replicator.despawned.push({ nodeId, peerIndex: network.local!.id!, data }); return 0; }, @@ -417,8 +406,8 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo if (!replicator) { moveCursorView(wasmCtx.cursorView, infoPtr); writeUint32(wasmCtx.cursorView, 0); - writeUint32(wasmCtx.cursorView, 0); - writeUint32(wasmCtx.cursorView, 0); + writeUint64(wasmCtx.cursorView, 0n); + writeUint64(wasmCtx.cursorView, 0n); writeUint32(wasmCtx.cursorView, 0); console.error("Undefined replicator."); return -1; @@ -445,8 +434,8 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo moveCursorView(wasmCtx.cursorView, infoPtr); writeUint32(wasmCtx.cursorView, nodeId); - writeUint32(wasmCtx.cursorView, networkId); - writeUint32(wasmCtx.cursorView, peerIndex); + writeUint64(wasmCtx.cursorView, BigInt(networkId)); + writeUint64(wasmCtx.cursorView, BigInt(peerIndex)); writeUint32(wasmCtx.cursorView, byteLength); return replicator.spawned.length; @@ -462,8 +451,8 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo if (!replicator) { moveCursorView(wasmCtx.cursorView, infoPtr); writeUint32(wasmCtx.cursorView, 0); - writeUint32(wasmCtx.cursorView, 0); - writeUint32(wasmCtx.cursorView, 0); + writeUint64(wasmCtx.cursorView, 0n); + writeUint64(wasmCtx.cursorView, 0n); writeUint32(wasmCtx.cursorView, 0); console.error("Undefined replicator."); return -1; @@ -490,8 +479,8 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo moveCursorView(wasmCtx.cursorView, infoPtr); writeUint32(wasmCtx.cursorView, nodeId); - writeUint32(wasmCtx.cursorView, networkId); - writeUint32(wasmCtx.cursorView, peerIndex); + writeUint64(wasmCtx.cursorView, BigInt(networkId)); + writeUint64(wasmCtx.cursorView, BigInt(peerIndex)); writeUint32(wasmCtx.cursorView, byteLength); return replicator.despawned.length; @@ -605,7 +594,7 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo moveCursorView(wasmCtx.cursorView, propsPtr); readExtensionsAndExtras(wasmCtx); - const networkId = readUint32(wasmCtx.cursorView); + const networkId = readUint64(wasmCtx.cursorView); const replicatorId = readUint32(wasmCtx.cursorView); const replicator = wasmCtx.resourceManager.replicators.get(replicatorId); @@ -616,7 +605,7 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo } addComponent(ctx.world, Networked, nodeId, true); - Networked.networkId[nodeId] = networkId; + Networked.networkId[nodeId] = Number(networkId); network.networkIdToEntityId.set(networkId, nodeId); addPrefabComponent(ctx.world, nodeId, replicator.prefabName); @@ -654,7 +643,7 @@ export function createWebSGNetworkModule(ctx: GameContext, wasmCtx: WASMModuleCo const messageView = createCursorView(new ArrayBuffer(10000)); function createScriptMessage(ctx: GameContext, packet: ArrayBuffer, binary: boolean) { - writeMetadata(messageView, binary ? NetworkAction.BinaryScriptMessage : NetworkAction.StringScriptMessage); + writeMessageType(messageView, binary ? NetworkMessage.BinaryScriptMessage : NetworkMessage.StringScriptMessage); serializeScriptMessage(messageView, packet); return sliceCursorView(messageView); } @@ -692,14 +681,15 @@ function deserializeScriptMessage(ctx: GameContext, v: CursorView, peerId: strin } } -function getPeerNode(ctx: GameContext, network: GameNetworkState, peerIndex: number) { - const peerId = network.indexToPeerId.get(peerIndex); +function getPeerNode(ctx: GameContext, network: GameNetworkState, peerId: PeerID) { + const peerInfo = tryGetPeerInfoById(network, peerId); + const peerKey = peerInfo.key; - if (!peerId) { + if (!peerKey) { return undefined; } - const eid = network.peerIdToEntityId.get(peerId); + const eid = peerInfo.entityId; if (!eid) { return undefined; diff --git a/src/engine/network/serialization.game.ts b/src/engine/network/serialization.game.ts index b45cfa505..caae68d9d 100644 --- a/src/engine/network/serialization.game.ts +++ b/src/engine/network/serialization.game.ts @@ -1,28 +1,28 @@ import { addComponent } from "bitecs"; import { - createCursorView, CursorView, moveCursorView, readArrayBuffer, readFloat32, - readFloat64, readString, readUint16, readUint32, + readUint64, readUint8, rewindCursorView, scrollCursorView, sliceCursorView, spaceUint16, spaceUint32, + spaceUint64, writeArrayBuffer, writeFloat32, - writeFloat64, writePropIfChanged, writeScalarPropIfChanged, writeString, writeUint32, + writeUint64, writeUint8, } from "../allocator/CursorView"; import { NOOP } from "../config.common"; @@ -31,15 +31,16 @@ import { getModule } from "../module/module.common"; import { Prefab, createPrefabEntity } from "../prefab/prefab.game"; import { checkBitflag } from "../utils/checkBitflag"; import { - ownedNetworkedQuery, - createdOwnedNetworkedQuery, + authoringNetworkedQuery, + spawnedNetworkeQuery, GameNetworkState, - deletedOwnedNetworkedQuery, - associatePeerWithEntity, + despawnedNetworkQuery, + NetworkID, + tryGetPeerId, } from "./network.game"; import { Networked } from "./NetworkComponents"; import { NetworkModule } from "./network.game"; -import { NetworkAction } from "./NetworkAction"; +import { NetworkMessage } from "./NetworkMessage"; import { waitUntil } from "../utils/waitUntil"; import { getRemoteResource, tryGetRemoteResource } from "../resource/resource.game"; import { @@ -57,35 +58,15 @@ import { Player } from "../player/Player"; import { addNametag } from "../player/nametags.game"; import { AudioEmitterType } from "../resource/schema"; -// ad-hoc messages view -const messageView = createCursorView(new ArrayBuffer(10000)); - const metadataTotalBytes = Uint8Array.BYTES_PER_ELEMENT + Float64Array.BYTES_PER_ELEMENT + Uint32Array.BYTES_PER_ELEMENT; -export const writeMessageType = (v: CursorView, type: NetworkAction) => writeUint8(v, type); - -export const writeElapsed = (v: CursorView) => writeFloat64(v, Date.now()); - -export const writeMetadata = (v: CursorView, type: NetworkAction) => { - writeMessageType(v, type); - writeElapsed(v); - // HACK: leave space for the input tick - scrollCursorView(v, Uint32Array.BYTES_PER_ELEMENT); -}; - -const _out: { type: number; elapsed: number; inputTick: number } = { type: 0, elapsed: 0, inputTick: 0 }; -export const readMetadata = (v: CursorView, out = _out) => { - out.type = readUint8(v); - out.elapsed = readFloat64(v); - // HACK?: read input tick processed from this peer to each packet - out.inputTick = readUint32(v); - return out; -}; +export const writeMessageType = (v: CursorView, type: NetworkMessage) => writeUint8(v, type); +export const readMessageType = (v: CursorView) => readUint8(v); /* Transform serialization */ -export const serializeTransformSnapshot = (v: CursorView, node: RemoteNode) => { +export const writeTransform = (v: CursorView, node: RemoteNode) => { const position = node.position; writeFloat32(v, position[0]); writeFloat32(v, position[1]); @@ -108,10 +89,10 @@ export const serializeTransformSnapshot = (v: CursorView, node: RemoteNode) => { return v; }; -export const deserializeTransformSnapshot = ( +export const readTransform = ( network: GameNetworkState, v: CursorView, - nid: number, + nid: NetworkID, node: RemoteNode | undefined ) => { if (node !== undefined) { @@ -190,7 +171,7 @@ export const serializeTransformChanged = (v: CursorView, node: RemoteNode) => { return changeMask > 0; }; -export const deserializeTransformChanged = (v: CursorView, nid: number, node: RemoteNode | undefined) => { +export const deserializeTransformChanged = (v: CursorView, nid: NetworkID, node: RemoteNode | undefined) => { if (node) { const changeMask = readUint16(v); let b = 0; @@ -222,14 +203,14 @@ export const deserializeTransformChanged = (v: CursorView, nid: number, node: Re export function createRemoteNetworkedEntity( ctx: GameContext, network: GameNetworkState, - nid: number, + nid: NetworkID, prefab: string ): RemoteNode { const node = createPrefabEntity(ctx, prefab); // assign networkId addComponent(ctx.world, Networked, node.eid, true); - Networked.networkId[node.eid] = nid; + Networked.networkId[node.eid] = Number(nid); network.networkIdToEntityId.set(nid, node.eid); addObjectToWorld(ctx, node); @@ -237,7 +218,7 @@ export function createRemoteNetworkedEntity( } function writeCreation(network: GameNetworkState, v: CursorView, eid: number) { - const nid = Networked.networkId[eid]; + const nid = BigInt(Networked.networkId[eid]); const prefabName = Prefab.get(eid); if (!prefabName) { @@ -245,7 +226,7 @@ function writeCreation(network: GameNetworkState, v: CursorView, eid: number) { throw new Error(`could not serialize creation for ${eid}, entity has no prefab`); } - writeUint32(v, nid); + writeUint64(v, nid); writeString(v, prefabName); const writeDataByteLength = spaceUint32(v); @@ -264,7 +245,7 @@ function writeCreation(network: GameNetworkState, v: CursorView, eid: number) { export function serializeCreatesSnapshot(ctx: GameContext, v: CursorView) { const network = getModule(ctx, NetworkModule); - const entities = ownedNetworkedQuery(ctx.world); + const entities = authoringNetworkedQuery(ctx.world); // TODO: optimize length written with maxEntities config writeUint32(v, entities.length); @@ -276,7 +257,7 @@ export function serializeCreatesSnapshot(ctx: GameContext, v: CursorView) { export function serializeCreates(ctx: GameContext, v: CursorView) { const network = getModule(ctx, NetworkModule); - const entities = createdOwnedNetworkedQuery(ctx.world); + const entities = spawnedNetworkeQuery(ctx.world); writeUint32(v, entities.length); for (let i = 0; i < entities.length; i++) { @@ -289,7 +270,7 @@ export function deserializeCreates(ctx: GameContext, v: CursorView, peerId: stri const network = getModule(ctx, NetworkModule); const count = readUint32(v); for (let i = 0; i < count; i++) { - const nid = readUint32(v); + const nid = readUint64(v); const prefabName = readString(v); const dataByteLength = readUint32(v); @@ -308,7 +289,7 @@ export function deserializeCreates(ctx: GameContext, v: CursorView, peerId: stri replicator.spawned.push({ networkId: nid, - peerIndex: network.peerIdToIndex.get(peerId)!, + peerIndex: tryGetPeerId(network, peerId), data, }); network.deferredUpdates.set(nid, []); @@ -321,14 +302,14 @@ export function deserializeCreates(ctx: GameContext, v: CursorView, peerId: stri /* Updates - Snapshot */ export function serializeUpdatesSnapshot(ctx: GameContext, v: CursorView) { - const entities = ownedNetworkedQuery(ctx.world); + const entities = authoringNetworkedQuery(ctx.world); writeUint32(v, entities.length); for (let i = 0; i < entities.length; i++) { const eid = entities[i]; - const nid = Networked.networkId[eid]; + const nid = BigInt(Networked.networkId[eid]); const node = tryGetRemoteResource(ctx, eid); - writeUint32(v, nid); - serializeTransformSnapshot(v, node); + writeUint64(v, nid); + writeTransform(v, node); } } @@ -336,26 +317,26 @@ export function deserializeUpdatesSnapshot(ctx: GameContext, v: CursorView) { const network = getModule(ctx, NetworkModule); const count = readUint32(v); for (let i = 0; i < count; i++) { - const nid = readUint32(v); + const nid = readUint64(v); const eid = network.networkIdToEntityId.get(nid) || NOOP; const node = getRemoteResource(ctx, eid); - deserializeTransformSnapshot(network, v, nid, node); + readTransform(network, v, nid, node); } } /* Updates - Changed */ export function serializeUpdatesChanged(ctx: GameContext, v: CursorView) { - const entities = ownedNetworkedQuery(ctx.world); + const entities = authoringNetworkedQuery(ctx.world); const writeCount = spaceUint32(v); let count = 0; for (let i = 0; i < entities.length; i++) { const eid = entities[i]; - const nid = Networked.networkId[eid]; + const nid = BigInt(Networked.networkId[eid]); const node = tryGetRemoteResource(ctx, eid); const rewind = rewindCursorView(v); - const writeNid = spaceUint32(v); + const writeNid = spaceUint64(v); const written = serializeTransformChanged(v, node); if (written) { writeNid(nid); @@ -371,7 +352,7 @@ export function deserializeUpdatesChanged(ctx: GameContext, v: CursorView) { const network = getModule(ctx, NetworkModule); const count = readUint32(v); for (let i = 0; i < count; i++) { - const nid = readUint32(v); + const nid = readUint64(v); const eid = network.networkIdToEntityId.get(nid) || NOOP; if (eid === NOOP) { @@ -386,12 +367,12 @@ export function deserializeUpdatesChanged(ctx: GameContext, v: CursorView) { /* Delete */ export function serializeDeletes(ctx: GameContext, v: CursorView) { - const entities = deletedOwnedNetworkedQuery(ctx.world); + const entities = despawnedNetworkQuery(ctx.world); writeUint32(v, entities.length); for (let i = 0; i < entities.length; i++) { const eid = entities[i]; - const nid = Networked.networkId[eid]; - writeUint32(v, nid); + const nid = BigInt(Networked.networkId[eid]); + writeUint64(v, nid); console.info("serialized deletion for nid", nid, "eid", eid, "prefab", Prefab.get(eid)); } } @@ -400,7 +381,7 @@ export function deserializeDeletes(ctx: GameContext, v: CursorView) { const network = getModule(ctx, NetworkModule); const count = readUint32(v); for (let i = 0; i < count; i++) { - const nid = readUint32(v); + const nid = readUint64(v); const eid = network.networkIdToEntityId.get(nid); const node = eid ? getRemoteResource(ctx, eid) : undefined; if (!node) { @@ -415,28 +396,29 @@ export function deserializeDeletes(ctx: GameContext, v: CursorView) { /* Update NetworkId Message */ -export const serializeUpdateNetworkId = (ctx: GameContext, v: CursorView, from: number, to: number) => { +export const serializeUpdateNetworkId = (ctx: GameContext, v: CursorView, from: NetworkID, to: NetworkID) => { console.info("serializeUpdateNetworkId", from, "->", to); - writeUint32(v, from); - writeUint32(v, to); + writeUint64(v, from); + writeUint64(v, to); }; export function deserializeUpdateNetworkId(ctx: GameContext, v: CursorView) { const network = getModule(ctx, NetworkModule); - const from = readUint32(v); - const to = readUint32(v); + const from = readUint64(v); + const to = readUint64(v); const eid = network.networkIdToEntityId.get(from); if (!eid) throw new Error("could not find entity for nid: " + from); - Networked.networkId[eid] = to; + Networked.networkId[eid] = Number(to); console.info("deserializeUpdateNetworkId", from, "->", to); } export function createUpdateNetworkIdMessage(ctx: GameContext, from: number, to: number) { - writeMetadata(messageView, NetworkAction.UpdateNetworkId); - serializeUpdateNetworkId(ctx, messageView, from, to); - return sliceCursorView(messageView); + const network = getModule(ctx, NetworkModule); + writeMessageType(network.cursorView, NetworkMessage.UpdateNetworkId); + serializeUpdateNetworkId(ctx, network.cursorView, BigInt(from), BigInt(to)); + return sliceCursorView(network.cursorView); } /* Player NetworkId Message */ @@ -444,18 +426,18 @@ export function createUpdateNetworkIdMessage(ctx: GameContext, from: number, to: export const serializeInformPlayerNetworkId = (ctx: GameContext, v: CursorView, peerId: string) => { console.info("serializeInformPlayerNetworkId", peerId); const network = getModule(ctx, NetworkModule); - const peerEid = network.peerIdToEntityId.get(peerId); - if (peerEid === undefined) { + const eid = network.peerIdToEntityId.get(peerId); + if (eid === undefined) { throw new Error(`could not send NetworkMessage.InformPlayerNetworkId, ${peerId} not set on peerIdToEntity map`); } - const peerNid = Networked.networkId[peerEid]; - if (peerNid === NOOP) { - throw new Error(`could not send NetworkMessage.InformPlayerNetworkId, ${peerEid} has no networkId assigned`); + const nid = BigInt(Networked.networkId[eid]); + if (!nid) { + throw new Error(`could not send NetworkMessage.InformPlayerNetworkId, ${eid} has no networkId assigned`); } writeString(v, peerId); - writeUint32(v, peerNid); + writeUint64(v, nid); }; export async function deserializeInformPlayerNetworkId(ctx: GameContext, v: CursorView) { @@ -463,7 +445,7 @@ export async function deserializeInformPlayerNetworkId(ctx: GameContext, v: Curs // read const peerId = readString(v); - const peerNid = readUint32(v); + const peerNid = BigInt(readUint64(v)); console.info("deserializeInformPlayerNetworkId for peer", peerId, peerNid); @@ -471,7 +453,7 @@ export async function deserializeInformPlayerNetworkId(ctx: GameContext, v: Curs // HACK: await the entity's creation const peid = await waitUntil(() => network.networkIdToEntityId.get(peerNid)); - associatePeerWithEntity(network, peerId, peid); + // associatePeerWithEntity(network, peerId, peid); addPlayerFromPeer(ctx, peid, peerId); addComponent(ctx.world, Player, peid); @@ -505,17 +487,16 @@ export async function deserializeInformPlayerNetworkId(ctx: GameContext, v: Curs peerNode.name = peerId; // if not our own avatar, add nametag - if (peerId !== network.peerId) { + if (peerId !== network.local) { addNametag(ctx, AVATAR_HEIGHT + AVATAR_HEIGHT / 3, peerNode, peerId); } } export function createInformXRModeMessage(ctx: GameContext, xrMode: XRMode) { - writeMetadata(messageView, NetworkAction.InformXRMode); - - serializeInformXRMode(messageView, xrMode); - - return sliceCursorView(messageView); + const network = getModule(ctx, NetworkModule); + writeMessageType(network.cursorView, NetworkMessage.InformXRMode); + serializeInformXRMode(network.cursorView, xrMode); + return sliceCursorView(network.cursorView); } export const serializeInformXRMode = (v: CursorView, xrMode: XRMode) => { writeUint8(v, xrMode); @@ -533,9 +514,10 @@ export const deserializeInformXRMode = (ctx: GameContext, v: CursorView, peerId: }; export function createInformPlayerNetworkIdMessage(ctx: GameContext, peerId: string) { - writeMetadata(messageView, NetworkAction.InformPlayerNetworkId); - serializeInformPlayerNetworkId(ctx, messageView, peerId); - return sliceCursorView(messageView); + const network = getModule(ctx, NetworkModule); + writeMessageType(network.cursorView, NetworkMessage.InformPlayerNetworkId); + serializeInformPlayerNetworkId(ctx, network.cursorView, peerId); + return sliceCursorView(network.cursorView); } /* Message Factories */ @@ -543,7 +525,7 @@ export function createInformPlayerNetworkIdMessage(ctx: GameContext, peerId: str // New Peer Snapshot Update export const createNewPeerSnapshotMessage = (ctx: GameContext, v: CursorView) => { - writeMetadata(v, NetworkAction.NewPeerSnapshot); + writeMessageType(v, NetworkMessage.NewPeerSnapshot); serializeCreatesSnapshot(ctx, v); serializeUpdatesSnapshot(ctx, v); return sliceCursorView(v); @@ -556,7 +538,7 @@ export const deserializeNewPeerSnapshot = (ctx: GameContext, v: CursorView, peer // Full Snapshot Update export const createFullSnapshotMessage = (ctx: GameContext, v: CursorView) => { - writeMetadata(v, NetworkAction.FullSnapshot); + writeMessageType(v, NetworkMessage.FullSnapshot); serializeCreates(ctx, v); serializeUpdatesSnapshot(ctx, v); serializeDeletes(ctx, v); @@ -573,7 +555,7 @@ export const deserializeSnapshot = (ctx: GameContext, v: CursorView, peerId: str // Changed State Update export const createFullChangedMessage = (ctx: GameContext, v: CursorView) => { - writeMetadata(v, NetworkAction.FullChanged); + writeMessageType(v, NetworkMessage.FullChanged); serializeCreates(ctx, v); serializeUpdatesChanged(ctx, v); serializeDeletes(ctx, v); @@ -591,7 +573,7 @@ export const deserializeFullChangedUpdate = (ctx: GameContext, v: CursorView, pe // Deletion Update export const createDeleteMessage = (ctx: GameContext, v: CursorView) => { - writeMetadata(v, NetworkAction.Delete); + writeMessageType(v, NetworkMessage.Delete); serializeDeletes(ctx, v); if (v.cursor <= metadataTotalBytes + Uint32Array.BYTES_PER_ELEMENT) { moveCursorView(v, 0); @@ -600,7 +582,7 @@ export const createDeleteMessage = (ctx: GameContext, v: CursorView) => { }; export const createCreateMessage = (ctx: GameContext, v: CursorView) => { - writeMetadata(v, NetworkAction.Create); + writeMessageType(v, NetworkMessage.Create); serializeCreates(ctx, v); if (v.cursor <= metadataTotalBytes + Uint32Array.BYTES_PER_ELEMENT) { moveCursorView(v, 0); @@ -609,7 +591,7 @@ export const createCreateMessage = (ctx: GameContext, v: CursorView) => { }; export const createUpdateChangedMessage = (ctx: GameContext, v: CursorView) => { - writeMetadata(v, NetworkAction.UpdateChanged); + writeMessageType(v, NetworkMessage.UpdateChanged); serializeUpdatesChanged(ctx, v); if (v.cursor <= metadataTotalBytes + Uint32Array.BYTES_PER_ELEMENT) { moveCursorView(v, 0); @@ -618,7 +600,7 @@ export const createUpdateChangedMessage = (ctx: GameContext, v: CursorView) => { }; export const createUpdateSnapshotMessage = (ctx: GameContext, v: CursorView) => { - writeMetadata(v, NetworkAction.UpdateSnapshot); + writeMessageType(v, NetworkMessage.UpdateSnapshot); serializeUpdatesSnapshot(ctx, v); if (v.cursor <= metadataTotalBytes + Uint32Array.BYTES_PER_ELEMENT) { moveCursorView(v, 0); diff --git a/src/engine/physics/physics.game.ts b/src/engine/physics/physics.game.ts index 163ff47c3..64c061ba1 100644 --- a/src/engine/physics/physics.game.ts +++ b/src/engine/physics/physics.game.ts @@ -16,10 +16,11 @@ import { import { ColliderType, MeshPrimitiveAttributeIndex, PhysicsBodyType } from "../resource/schema"; import { getAccessorArrayView, scaleVec3Array } from "../common/accessor"; import { updateMatrixWorld } from "../component/transform"; -import { Player } from "../player/Player"; +import { OurPlayer, Player } from "../player/Player"; import { getRotationNoAlloc } from "../utils/getRotationNoAlloc"; import { dynamicObjectCollisionGroups, staticRigidBodyCollisionGroups } from "./CollisionGroups"; import { updatePhysicsDebugBuffers } from "../renderer/renderer.game"; +import { Authoring, Networked } from "../network/NetworkComponents"; export type CollisionHandler = (eid1: number, eid2: number, handle1: number, handle2: number, started: boolean) => void; @@ -60,7 +61,7 @@ export const KinematicBody = defineComponent(); // data flows from body->transform export const RigidBody = defineComponent(); -// daata doesn't change +// data doesn't change export const StaticBody = defineComponent(); const _v = new Vector3(); @@ -131,13 +132,23 @@ export function PhysicsSystem(ctx: GameContext) { const eid = physicsEntities[i]; const node = getRemoteResource(ctx, eid); - if (!node || !node.physicsBody?.body) { + // TODO: add Not(StaticBody) to phys query + const isStatic = hasComponent(ctx.world, StaticBody, eid); + if (!node || !node.physicsBody?.body || isStatic) { continue; } const body = node.physicsBody.body; const bodyType = body.bodyType(); + const isOurPlayer = hasComponent(ctx.world, OurPlayer, eid); + const isRemoteNonPlayer = + hasComponent(ctx.world, Networked, eid) && + !hasComponent(ctx.world, Player, eid) && + !hasComponent(ctx.world, Authoring, eid); + const isDynamic = hasComponent(ctx.world, RigidBody, eid); + const isKinematic = hasComponent(ctx.world, KinematicBody, eid); + if (bodyType !== RAPIER.RigidBodyType.Fixed) { // sync velocity const linvel = body.linvel(); @@ -147,13 +158,14 @@ export function PhysicsSystem(ctx: GameContext) { velocity[2] = linvel.z; } - const isPlayer = hasComponent(ctx.world, Player, eid); - - if (bodyType === RAPIER.RigidBodyType.Dynamic || isPlayer) { + if (isOurPlayer) { + applyRigidBodyToTransform(body, node); + } else if (isRemoteNonPlayer) { + applyTransformToRigidBody(body, node); + } else if (isDynamic) { applyRigidBodyToTransform(body, node); - } else if (bodyType === RAPIER.RigidBodyType.KinematicPositionBased && !isPlayer) { + } else if (isKinematic) { updateMatrixWorld(node); - getRotationNoAlloc(_worldQuat, node.worldMatrix); _q.fromArray(_worldQuat); body.setNextKinematicRotation(_q); @@ -329,8 +341,6 @@ export function addPhysicsBody( node.physicsBody.body = body; - handleToEid.set(body.handle, node.eid); - if (node.collider) { const colliderDescriptions = createNodeColliderDescriptions(node); diff --git a/src/engine/player/CameraRig.ts b/src/engine/player/CameraRig.ts index e16ba49ee..d4a7e3a65 100644 --- a/src/engine/player/CameraRig.ts +++ b/src/engine/player/CameraRig.ts @@ -217,7 +217,6 @@ export function stopOrbit(ctx: GameContext) { return; } - const input = getModule(ctx, InputModule); const physics = getModule(ctx, PhysicsModule); const camRigModule = getModule(ctx, PlayerModule); @@ -230,7 +229,7 @@ export function stopOrbit(ctx: GameContext) { const ourPlayer = ourPlayerQuery(ctx.world)[0]; const node = tryGetRemoteResource(ctx, ourPlayer); - embodyAvatar(ctx, physics, input, node); + embodyAvatar(ctx, physics, node); ctx.sendMessage(Thread.Main, { type: CameraRigMessage.StopOrbit }); } @@ -444,6 +443,8 @@ export function CameraRigSystem(ctx: GameContext) { continue; } + // console.log("pitch.target", pitch.target); + applyPitch(ctx, input, pitch); } @@ -457,6 +458,8 @@ export function CameraRigSystem(ctx: GameContext) { continue; } + // console.log("yaw.target", yaw.target); + applyYaw(ctx, input, yaw); } @@ -469,6 +472,8 @@ export function CameraRigSystem(ctx: GameContext) { continue; } + // console.log("zoom.target", zoom.target); + applyZoom(ctx, input, zoom); } @@ -482,6 +487,7 @@ function exitQueryCleanup(ctx: GameContext, query: Query, component: Map(ctx, playerRigEid); - const camera = getCamera(ctx, playerRig); + const camera = tryGetCamera(ctx, playerRig); const body = playerRig.physicsBody?.body; if (!body) { diff --git a/src/engine/player/PlayerRig.ts b/src/engine/player/PlayerRig.ts index 4129a48c3..888cf3978 100644 --- a/src/engine/player/PlayerRig.ts +++ b/src/engine/player/PlayerRig.ts @@ -1,20 +1,24 @@ import RAPIER from "@dimforge/rapier3d-compat"; -import { addComponent, defineComponent } from "bitecs"; +import { addComponent } from "bitecs"; import { quat, vec3 } from "gl-matrix"; import { addInteractableComponent, GRAB_DISTANCE } from "../../plugins/interaction/interaction.game"; -import { getSpawnPoints, spawnPointQuery } from "../../plugins/thirdroom/thirdroom.game"; +import { + getSpawnPoints, + spawnPointQuery, + ThirdRoomModule, + ThirdRoomModuleState, +} from "../../plugins/thirdroom/thirdroom.game"; import { addChild } from "../component/transform"; import { GameContext } from "../GameTypes"; import { createNodeFromGLTFURI } from "../gltf/gltf.game"; -import { GameInputModule } from "../input/input.game"; import { createLineMesh } from "../mesh/mesh.game"; import { getModule } from "../module/module.common"; -import { GameNetworkState, associatePeerWithEntity, NetworkModule, setLocalPeerId } from "../network/network.game"; -import { Owned, Networked } from "../network/NetworkComponents"; +import { NetworkModule, tryGetPeerInfoById } from "../network/network.game"; +import { Authoring, Networked } from "../network/NetworkComponents"; import { playerCollisionGroups } from "../physics/CollisionGroups"; -import { addPhysicsBody, addPhysicsCollider, PhysicsModule, PhysicsModuleState } from "../physics/physics.game"; -import { createPrefabEntity, PrefabType, registerPrefab } from "../prefab/prefab.game"; +import { addPhysicsBody, addPhysicsCollider, PhysicsModule } from "../physics/physics.game"; +import { PrefabType, registerPrefab } from "../prefab/prefab.game"; import { RemoteNode, RemoteMaterial, @@ -24,6 +28,7 @@ import { RemoteAudioSource, RemoteCollider, RemotePhysicsBody, + removeObjectFromWorld, } from "../resource/RemoteResources"; import { getRemoteResource } from "../resource/resource.game"; import { @@ -43,70 +48,80 @@ import { embodyAvatar } from "./embodyAvatar"; import { addFlyControls } from "./FlyCharacterController"; import { addKinematicControls } from "./KinematicCharacterController"; import { addNametag } from "./nametags.game"; -import { Player, OurPlayer } from "./Player"; +import { Player } from "./Player"; +import { XRControllerComponent, XRHeadComponent, XRRayComponent } from "./XRComponents"; +import { createNetworkReplicator } from "../network/NetworkReplicator"; +import { transformCodec } from "../network/NetworkMessage"; +import { isHost } from "../network/network.common"; const AVATAR_CAPSULE_HEIGHT = 1; const AVATAR_CAPSULE_RADIUS = 0.35; export const AVATAR_HEIGHT = AVATAR_CAPSULE_HEIGHT + AVATAR_CAPSULE_RADIUS * 2; const AVATAR_CAMERA_OFFSET = 0.06; -export function registerPlayerPrefabs(ctx: GameContext) { - registerPrefab(ctx, { - name: "avatar", - type: PrefabType.Avatar, - create: (ctx: GameContext) => { - const physics = getModule(ctx, PhysicsModule); - const spawnPoints = spawnPointQuery(ctx.world); - - const container = new RemoteNode(ctx.resourceManager); - const rig = createNodeFromGLTFURI(ctx, "/gltf/full-animation-rig.glb"); - - addChild(container, rig); - - quat.fromEuler(rig.quaternion, 0, 180, 0); +const avatarFactory = (ctx: GameContext) => { + const physics = getModule(ctx, PhysicsModule); + + const container = new RemoteNode(ctx.resourceManager); + const rig = createNodeFromGLTFURI(ctx, "/gltf/full-animation-rig.glb"); + + addChild(container, rig); + + quat.fromEuler(rig.quaternion, 0, 180, 0); + + addPhysicsCollider( + ctx.world, + container, + new RemoteCollider(ctx.resourceManager, { + type: ColliderType.Capsule, + height: AVATAR_CAPSULE_HEIGHT + 0.15, + radius: AVATAR_CAPSULE_RADIUS, + activeEvents: RAPIER.ActiveEvents.COLLISION_EVENTS, + collisionGroups: playerCollisionGroups, + offset: [0, AVATAR_CAPSULE_HEIGHT - 0.15, 0], + }) + ); + + addPhysicsBody( + ctx.world, + physics, + container, + new RemotePhysicsBody(ctx.resourceManager, { + type: PhysicsBodyType.Kinematic, + }) + ); + + addInteractableComponent(ctx, physics, container, InteractableType.Player); + + addComponent(ctx.world, AvatarRef, container.eid); + AvatarRef.eid[container.eid] = rig.eid; + + // TODO: reuse audio sources+data + container.audioEmitter = new RemoteAudioEmitter(ctx.resourceManager, { + type: AudioEmitterType.Positional, + sources: [ + new RemoteAudioSource(ctx.resourceManager, { + audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-01.ogg" }), + }), + new RemoteAudioSource(ctx.resourceManager, { + audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-02.ogg" }), + }), + new RemoteAudioSource(ctx.resourceManager, { + audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-03.ogg" }), + }), + new RemoteAudioSource(ctx.resourceManager, { + audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-04.ogg" }), + }), + ], + }); - // on container - const characterControllerType = SceneCharacterControllerComponent.get( - ctx.worldResource.environment!.publicScene!.eid - )?.type; - if (characterControllerType === CharacterControllerType.Fly || spawnPoints.length === 0) { - addFlyControls(ctx, container.eid); - } else { - addKinematicControls(ctx, container.eid); - } + return container; +}; - addCameraRig(ctx, container, CameraRigType.PointerLock, [0, AVATAR_HEIGHT - AVATAR_CAMERA_OFFSET, 0]); - - addPhysicsCollider( - ctx.world, - container, - new RemoteCollider(ctx.resourceManager, { - type: ColliderType.Capsule, - height: AVATAR_CAPSULE_HEIGHT + 0.15, - radius: AVATAR_CAPSULE_RADIUS, - activeEvents: RAPIER.ActiveEvents.COLLISION_EVENTS, - collisionGroups: playerCollisionGroups, - offset: [0, AVATAR_CAPSULE_HEIGHT - 0.15, 0], - }) - ); - - addPhysicsBody( - ctx.world, - physics, - container, - new RemotePhysicsBody(ctx.resourceManager, { - type: PhysicsBodyType.Kinematic, - }) - ); - - addInteractableComponent(ctx, physics, container, InteractableType.Player); - - addComponent(ctx.world, AvatarRef, container.eid); - AvatarRef.eid[container.eid] = rig.eid; - - return container; - }, - }); +export function registerPlayerPrefabs(ctx: GameContext, thirdroom: ThirdRoomModuleState) { + thirdroom.replicators = { + avatar: createNetworkReplicator(ctx, avatarFactory, transformCodec), + }; registerPrefab(ctx, { name: "xr-head", @@ -174,11 +189,7 @@ export function registerPlayerPrefabs(ctx: GameContext) { }); } -export const XRControllerComponent = defineComponent(); -export const XRHeadComponent = defineComponent(); -export const XRRayComponent = defineComponent(); - -export function addPlayerFromPeer(ctx: GameContext, eid: number, peerId: string) { +export function addPlayerFromPeer(ctx: GameContext, eid: number, peerKey: string) { const network = getModule(ctx, NetworkModule); addComponent(ctx.world, Player, eid); @@ -202,81 +213,133 @@ export function addPlayerFromPeer(ctx: GameContext, eid: number, peerId: string) }), new RemoteAudioSource(ctx.resourceManager, { audio: new RemoteAudioData(ctx.resourceManager, { - uri: `mediastream:${peerId}`, + uri: `mediastream:${peerKey}`, }), autoPlay: true, }), ], }); - peerNode.name = peerId; + peerNode.name = peerKey; // if not our own avatar, add nametag - if (peerId !== network.peerId) { - addNametag(ctx, AVATAR_HEIGHT + AVATAR_HEIGHT / 3, peerNode, peerId); + if (peerKey !== network.local?.key) { + addNametag(ctx, AVATAR_HEIGHT + AVATAR_HEIGHT / 3, peerNode, peerKey); + } +} + +export function teleportToSpawnPoint(ctx: GameContext, rig: RemoteNode) { + const spawnPoints = getSpawnPoints(ctx); + + if (spawnPoints.length > 0) { + spawnEntity(spawnPoints, rig); + } else { + teleportEntity(rig, vec3.fromValues(0, 0, 0), quat.create()); } } -export function loadPlayerRig(ctx: GameContext, physics: PhysicsModuleState, input: GameInputModule) { - ctx.worldResource.activeCameraNode = undefined; +export function SpawnAvatarSystem(ctx: GameContext) { + const thirdroom = getModule(ctx, ThirdRoomModule); + const physics = getModule(ctx, PhysicsModule); + const network = getModule(ctx, NetworkModule); - const rig = createPrefabEntity(ctx, "avatar"); + const spawned = thirdroom.replicators!.avatar.spawned; - // setup positional audio emitter for footsteps - rig.audioEmitter = new RemoteAudioEmitter(ctx.resourceManager, { - type: AudioEmitterType.Positional, - sources: [ - new RemoteAudioSource(ctx.resourceManager, { - audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-01.ogg" }), - }), - new RemoteAudioSource(ctx.resourceManager, { - audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-02.ogg" }), - }), - new RemoteAudioSource(ctx.resourceManager, { - audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-03.ogg" }), - }), - new RemoteAudioSource(ctx.resourceManager, { - audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-04.ogg" }), - }), - ], - }); + let spawn; + while ((spawn = spawned.dequeue())) { + const avatar = spawn.node; - addComponent(ctx.world, Player, rig.eid); - addComponent(ctx.world, OurPlayer, rig.eid); + addObjectToWorld(ctx, avatar); - addObjectToWorld(ctx, rig); + const localPeerId = network.local?.id; + const authorId = BigInt(Networked.authorId[avatar.eid]); + const hosting = isHost(network); - embodyAvatar(ctx, physics, input, rig); + console.log("spawned dequeue ===="); + console.log("avatar.eid", avatar.eid); + console.log("localPeerId", localPeerId); + console.log("authorId", authorId); + console.log("hosting", hosting); - return rig; -} + const authoring = authorId === localPeerId; + if (authoring) { + // if we aren't hosting + if (!hosting) { + // add Authoring component so that our avatar updates are sent to the host (client-side authority) + addComponent(ctx.world, Authoring, avatar.eid); + Networked.authorId[avatar.eid] = Number(localPeerId); + } -export function loadNetworkedPlayerRig( - ctx: GameContext, - physics: PhysicsModuleState, - input: GameInputModule, - network: GameNetworkState, - localPeerId: string -) { - const rig = loadPlayerRig(ctx, physics, input); - const eid = rig.eid; - setLocalPeerId(ctx, localPeerId); - associatePeerWithEntity(network, localPeerId, eid); - rig.name = localPeerId; - // TODO: add Authoring component for authoritatively controlled entities as a host, - // use Owned to distinguish actual ownership on all clients - // Networked component isn't reset when removed so reset on add - addComponent(ctx.world, Owned, eid); - addComponent(ctx.world, Networked, eid, true); - return rig; + // add appropriate controls + const characterControllerType = SceneCharacterControllerComponent.get( + ctx.worldResource.environment!.publicScene!.eid + )?.type; + const spawnPoints = spawnPointQuery(ctx.world); + if (characterControllerType === CharacterControllerType.Fly || spawnPoints.length === 0) { + addFlyControls(ctx, avatar.eid); + } else { + addKinematicControls(ctx, avatar.eid); + } + + // add camera rig and embody the avatar + // TODO: maybe refactor this portion + addCameraRig(ctx, avatar, CameraRigType.PointerLock, [0, AVATAR_HEIGHT - AVATAR_CAMERA_OFFSET, 0]); + embodyAvatar(ctx, physics, avatar); + } else { + const peerInfo = tryGetPeerInfoById(network, authorId); + const peerKey = peerInfo.key; + + avatar.name = peerKey; + + addNametag(ctx, AVATAR_HEIGHT + AVATAR_HEIGHT / 3, avatar, peerKey); + addComponent(ctx.world, Player, avatar.eid); + + // TODO: fix audio emitter disposal + // avatar.audioEmitter!.sources.push( + // new RemoteAudioSource(ctx.resourceManager, { + // audio: new RemoteAudioData(ctx.resourceManager, { + // uri: `mediastream:${peerId}`, + // }), + // autoPlay: true, + // }) + // ); + + // HACK + avatar.audioEmitter = new RemoteAudioEmitter(ctx.resourceManager, { + type: AudioEmitterType.Positional, + sources: [ + new RemoteAudioSource(ctx.resourceManager, { + audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-01.ogg" }), + }), + new RemoteAudioSource(ctx.resourceManager, { + audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-02.ogg" }), + }), + new RemoteAudioSource(ctx.resourceManager, { + audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-03.ogg" }), + }), + new RemoteAudioSource(ctx.resourceManager, { + audio: new RemoteAudioData(ctx.resourceManager, { uri: "/audio/footstep-04.ogg" }), + }), + new RemoteAudioSource(ctx.resourceManager, { + audio: new RemoteAudioData(ctx.resourceManager, { + uri: `mediastream:${peerKey}`, + }), + autoPlay: true, + }), + ], + }); + } + } } -export function spawnPlayer(ctx: GameContext, rig: RemoteNode) { - const spawnPoints = getSpawnPoints(ctx); +export function DespawnAvatarSystem(ctx: GameContext) { + const thirdroom = getModule(ctx, ThirdRoomModule); - if (spawnPoints.length > 0) { - spawnEntity(spawnPoints, rig); - } else { - teleportEntity(rig, vec3.fromValues(0, 0, 0), quat.create()); + const despawned = thirdroom.replicators!.avatar.despawned; + + let avatar; + while ((avatar = despawned.dequeue())) { + console.log("despawning avatar", avatar.eid); + removeObjectFromWorld(ctx, avatar); } } diff --git a/src/engine/player/XRComponents.ts b/src/engine/player/XRComponents.ts new file mode 100644 index 000000000..e4f9d91b2 --- /dev/null +++ b/src/engine/player/XRComponents.ts @@ -0,0 +1,5 @@ +import { defineComponent } from "bitecs"; + +export const XRControllerComponent = defineComponent(); +export const XRHeadComponent = defineComponent(); +export const XRRayComponent = defineComponent(); diff --git a/src/engine/player/embodyAvatar.ts b/src/engine/player/embodyAvatar.ts index 9c14f2fd9..44a7da38a 100644 --- a/src/engine/player/embodyAvatar.ts +++ b/src/engine/player/embodyAvatar.ts @@ -2,39 +2,34 @@ import { removeComponent, addComponent } from "bitecs"; import { removeInteractableComponent } from "../../plugins/interaction/interaction.game"; import { GameContext } from "../GameTypes"; -import { GameInputModule } from "../input/input.game"; import { addXRAvatarRig } from "../input/WebXRAvatarRigSystem"; import { PhysicsModuleState } from "../physics/physics.game"; import { RemoteNode } from "../resource/RemoteResources"; -import { tryGetRemoteResource } from "../resource/resource.game"; +import { getRemoteResource } from "../resource/resource.game"; import { AvatarRef } from "./components"; -import { getCamera } from "./getCamera"; -import { getNametag, NametagAnchor } from "./nametags.game"; -import { OurPlayer } from "./Player"; +import { tryGetCamera } from "./getCamera"; +import { NametagAnchor, getNametag } from "./nametags.game"; +import { OurPlayer, Player } from "./Player"; -// TODO: move this to a plugin (along with InformPlayerNetworkId OR register another hook into InformPlayerNetworkId) -export function embodyAvatar(ctx: GameContext, physics: PhysicsModuleState, input: GameInputModule, node: RemoteNode) { +export function embodyAvatar(ctx: GameContext, physics: PhysicsModuleState, node: RemoteNode) { // remove the nametag - try { - const nametag = getNametag(ctx, node); - removeComponent(ctx.world, NametagAnchor, nametag.eid); - } catch {} + const nametag = getNametag(ctx, node); + if (nametag) removeComponent(ctx.world, NametagAnchor, nametag.eid); // hide our avatar - try { - const avatarEid = AvatarRef.eid[node.eid]; - const avatar = tryGetRemoteResource(ctx, avatarEid); - avatar.visible = false; - } catch {} + const avatarEid = AvatarRef.eid[node.eid]; + const avatar = getRemoteResource(ctx, avatarEid); + if (avatar) avatar.visible = false; // mark entity as our player entity addComponent(ctx.world, OurPlayer, node.eid); + addComponent(ctx.world, Player, node.eid); // disable the collision group so we are unable to focus our own rigidbody removeInteractableComponent(ctx, physics, node); - // set the active camera & input controller to this entity's - ctx.worldResource.activeCameraNode = getCamera(ctx, node); + // set the active camera + ctx.worldResource.activeCameraNode = tryGetCamera(ctx, node); ctx.worldResource.activeAvatarNode = node; addXRAvatarRig(ctx.world, node.eid); diff --git a/src/engine/player/getCamera.ts b/src/engine/player/getCamera.ts index c1cf9a518..d9775f035 100644 --- a/src/engine/player/getCamera.ts +++ b/src/engine/player/getCamera.ts @@ -2,16 +2,18 @@ import { defineComponent, Types } from "bitecs"; import { GameContext } from "../GameTypes"; import { RemoteNode } from "../resource/RemoteResources"; -import { tryGetRemoteResource } from "../resource/resource.game"; +import { getRemoteResource, tryGetRemoteResource } from "../resource/resource.game"; export const CameraRef = defineComponent({ eid: Types.eid }); -/** - * Obtains the last added camera on the provided entity if one exists, throws if not - */ -export function getCamera(ctx: GameContext, root: RemoteNode): RemoteNode { +export function tryGetCamera(ctx: GameContext, root: RemoteNode): RemoteNode { const cameraEid = CameraRef.eid[root.eid]; if (!cameraEid) throw new Error(`CameraRef not found on node "${root.name}"`); const camera = tryGetRemoteResource(ctx, cameraEid); return camera; } + +export function getCamera(ctx: GameContext, root: RemoteNode): RemoteNode | undefined { + const cameraEid = CameraRef.eid[root.eid]; + return getRemoteResource(ctx, cameraEid); +} diff --git a/src/engine/player/nametags.game.ts b/src/engine/player/nametags.game.ts index e220b8cdc..12b1c9797 100644 --- a/src/engine/player/nametags.game.ts +++ b/src/engine/player/nametags.game.ts @@ -113,9 +113,14 @@ export function addNametag(ctx: GameContext, height: number, node: RemoteNode, l return nametag; } -export function getNametag(ctx: GameContext, parent: RemoteNode) { +export function tryGetNametag(ctx: GameContext, parent: RemoteNode) { const nametagEid = NametagRef.eid[parent.eid]; if (!nametagEid) throw new Error(`NametagRef not found on node "${parent.name}"`); const nametag = tryGetRemoteResource(ctx, nametagEid); return nametag; } + +export function getNametag(ctx: GameContext, parent: RemoteNode) { + const nametagEid = NametagRef.eid[parent.eid]; + return getRemoteResource(ctx, nametagEid); +} diff --git a/src/engine/renderer/node.ts b/src/engine/renderer/node.ts index 406f10cd7..2b8101847 100644 --- a/src/engine/renderer/node.ts +++ b/src/engine/renderer/node.ts @@ -11,10 +11,11 @@ const tempQuaternion = new Quaternion(); const tempScale = new Vector3(); export function updateTransformFromNode(ctx: RenderContext, node: RenderNode, object3D: Object3D) { - if (node.skipLerp) { - setTransformFromNode(node, object3D); - return; - } + // TODO + // if (node.skipLerp) { + setTransformFromNode(node, object3D); + return; + // } const frameRate = 1 / ctx.dt; const lerpAlpha = clamp(tickRate / frameRate, 0, 1); diff --git a/src/engine/resource/RemoteResources.ts b/src/engine/resource/RemoteResources.ts index a86bf3d47..ea195ef72 100644 --- a/src/engine/resource/RemoteResources.ts +++ b/src/engine/resource/RemoteResources.ts @@ -169,7 +169,6 @@ export class RemotePhysicsBody extends defineRemoteResourceClass(PhysicsBodyReso physics.handleToEid.delete(collider.handle); } - physics.handleToEid.delete(this.body.handle); physics.physicsWorld.removeRigidBody(this.body); } diff --git a/src/engine/scripting/emscripten/build/scripting-runtime.wasm b/src/engine/scripting/emscripten/build/scripting-runtime.wasm index 6bfe997d0..ffc3592bd 100755 Binary files a/src/engine/scripting/emscripten/build/scripting-runtime.wasm and b/src/engine/scripting/emscripten/build/scripting-runtime.wasm differ diff --git a/src/engine/scripting/emscripten/build/scripting-runtime.wasm.symbols b/src/engine/scripting/emscripten/build/scripting-runtime.wasm.symbols index 14bdb2d85..73a1322d1 100644 --- a/src/engine/scripting/emscripten/build/scripting-runtime.wasm.symbols +++ b/src/engine/scripting/emscripten/build/scripting-runtime.wasm.symbols @@ -258,8 +258,8 @@ 257:websg_replicator_spawn_local 258:websg_replicator_despawn_local 259:__wasi_clock_time_get -260:__wasi_fd_close -261:__wasi_proc_exit +260:__wasi_proc_exit +261:__wasi_fd_close 262:__wasi_environ_sizes_get 263:__wasi_environ_get 264:__wasi_fd_write @@ -1565,9 +1565,9 @@ 1564:__syscall_openat 1565:__syscall_fstat64 1566:emscripten_resize_heap -1567:__errno_location -1568:__map_file -1569:_Exit +1567:_Exit +1568:__errno_location +1569:__map_file 1570:acos 1571:R 1572:acosh diff --git a/src/engine/scripting/emscripten/src/js-runtime/websg-networking/network.c b/src/engine/scripting/emscripten/src/js-runtime/websg-networking/network.c index 77ab124ad..b837bf435 100644 --- a/src/engine/scripting/emscripten/src/js-runtime/websg-networking/network.c +++ b/src/engine/scripting/emscripten/src/js-runtime/websg-networking/network.c @@ -67,7 +67,7 @@ static JSValue js_websg_network_broadcast(JSContext *ctx, JSValueConst this_val, static JSValue js_websg_network_get_host(JSContext *ctx, JSValueConst this_val) { WebSGNetworkData *network_data = JS_GetOpaque2(ctx, this_val, js_websg_network_class_id); - uint32_t peer_index = websg_network_get_host_peer_index(); + uint64_t peer_index = websg_network_get_host_peer_index(); if (peer_index == 0) { return JS_UNDEFINED; @@ -78,7 +78,7 @@ static JSValue js_websg_network_get_host(JSContext *ctx, JSValueConst this_val) static JSValue js_websg_network_get_local(JSContext *ctx, JSValueConst this_val) { WebSGNetworkData *network_data = JS_GetOpaque2(ctx, this_val, js_websg_network_class_id); - uint32_t peer_index = websg_network_get_local_peer_index(); + uint64_t peer_index = websg_network_get_local_peer_index(); if (peer_index == 0) { return JS_UNDEFINED; @@ -163,8 +163,8 @@ JSValue js_websg_new_network(JSContext *ctx) { return network; } -int32_t js_websg_network_local_peer_entered(JSContext *ctx, JSValue network) { - uint32_t local_peer_index = websg_network_get_local_peer_index(); +int64_t js_websg_network_local_peer_entered(JSContext *ctx, JSValue network) { + uint64_t local_peer_index = websg_network_get_local_peer_index(); if (local_peer_index == -1) { return -1; @@ -174,12 +174,12 @@ int32_t js_websg_network_local_peer_entered(JSContext *ctx, JSValue network) { JSValue local_peer = js_websg_create_peer(ctx, network_data, local_peer_index); - JS_SetPropertyUint32(ctx, network_data->peers, local_peer_index, local_peer); + JS_SetPropertyInt64(ctx, network_data->peers, (int64_t)local_peer_index, local_peer); return 0; } -int32_t js_websg_network_peer_entered(JSContext *ctx, JSValue network, uint32_t peer_index) { +int64_t js_websg_network_peer_entered(JSContext *ctx, JSValue network, uint64_t peer_index) { WebSGNetworkData *network_data = JS_GetOpaque2(ctx, network, js_websg_network_class_id); JSValue peer = js_websg_create_peer(ctx, network_data, peer_index); @@ -188,7 +188,7 @@ int32_t js_websg_network_peer_entered(JSContext *ctx, JSValue network, uint32_t return -1; } - JS_SetPropertyUint32(ctx, network_data->peers, peer_index, peer); + JS_SetPropertyInt64(ctx, network_data->peers, (int64_t)peer_index, peer); JSValue network_on_peer_entered_func = JS_GetPropertyStr(ctx, network, "onpeerentered"); @@ -210,7 +210,7 @@ int32_t js_websg_network_peer_entered(JSContext *ctx, JSValue network, uint32_t } } -int32_t js_websg_network_peer_exited(JSContext *ctx, JSValue network, uint32_t peer_index) { +int64_t js_websg_network_peer_exited(JSContext *ctx, JSValue network, uint64_t peer_index) { WebSGNetworkData *network_data = JS_GetOpaque2(ctx, network, js_websg_network_class_id); JSValue peer = js_websg_remove_peer(ctx, network_data, peer_index); diff --git a/src/engine/scripting/emscripten/src/js-runtime/websg-networking/network.h b/src/engine/scripting/emscripten/src/js-runtime/websg-networking/network.h index f1568e469..b02ed03ea 100644 --- a/src/engine/scripting/emscripten/src/js-runtime/websg-networking/network.h +++ b/src/engine/scripting/emscripten/src/js-runtime/websg-networking/network.h @@ -16,10 +16,10 @@ JSValue js_websg_new_network(JSContext *ctx); void js_websg_network(JSContext *ctx, JSValue websg_networking); -int32_t js_websg_network_local_peer_entered(JSContext *ctx, JSValue network); +int64_t js_websg_network_local_peer_entered(JSContext *ctx, JSValue network); -int32_t js_websg_network_peer_entered(JSContext *ctx, JSValue network, uint32_t peer_index); +int64_t js_websg_network_peer_entered(JSContext *ctx, JSValue network, uint64_t peer_index); -int32_t js_websg_network_peer_exited(JSContext *ctx, JSValue network, uint32_t peer_index); +int64_t js_websg_network_peer_exited(JSContext *ctx, JSValue network, uint64_t peer_index); #endif diff --git a/src/engine/scripting/emscripten/src/js-runtime/websg-worker.c b/src/engine/scripting/emscripten/src/js-runtime/websg-worker.c index b4a909a3c..b14b65262 100644 --- a/src/engine/scripting/emscripten/src/js-runtime/websg-worker.c +++ b/src/engine/scripting/emscripten/src/js-runtime/websg-worker.c @@ -144,13 +144,13 @@ export int32_t websg_update(float_t dt, float_t time) { } } -export int32_t websg_peer_entered(uint32_t peer_index) { +export int32_t websg_peer_entered(uint64_t peer_index) { JSValue global = JS_GetGlobalObject(ctx); JSValue network = JS_GetPropertyStr(ctx, global, "network"); return js_websg_network_peer_entered(ctx, network, peer_index); } -export int32_t websg_peer_exited(uint32_t peer_index) { +export int32_t websg_peer_exited(uint64_t peer_index) { JSValue global = JS_GetGlobalObject(ctx); JSValue network = JS_GetPropertyStr(ctx, global, "network"); return js_websg_network_peer_exited(ctx, network, peer_index); diff --git a/src/engine/scripting/emscripten/src/websg-networking.h b/src/engine/scripting/emscripten/src/websg-networking.h index 02b25f88d..b5f8dfaf0 100644 --- a/src/engine/scripting/emscripten/src/websg-networking.h +++ b/src/engine/scripting/emscripten/src/websg-networking.h @@ -23,15 +23,15 @@ import_websg_networking(peer_is_host) int32_t websg_peer_is_host(uint32_t peer_i import_websg_networking(peer_is_local) int32_t websg_peer_is_local(uint32_t peer_index); import_websg_networking(peer_send) int32_t websg_peer_send(uint32_t peer_index, uint8_t *packet, uint32_t byte_length, uint32_t binary, uint32_t reliable); -import_websg_networking(network_get_host_peer_index) uint32_t websg_network_get_host_peer_index(); -import_websg_networking(network_get_local_peer_index) uint32_t websg_network_get_local_peer_index(); +import_websg_networking(network_get_host_peer_index) uint64_t websg_network_get_host_peer_index(); +import_websg_networking(network_get_local_peer_index) uint64_t websg_network_get_local_peer_index(); import_websg_networking(network_broadcast) int32_t websg_network_broadcast(uint8_t *packet, uint32_t byte_length, uint32_t binary, uint32_t reliable); import_websg_networking(network_listen) network_listener_id_t websg_network_listen(); import_websg_networking(network_listener_close) int32_t websg_network_listener_close(network_listener_id_t listener_id); typedef struct NetworkMessageInfo { - uint32_t peer_index; + uint64_t peer_index; uint32_t byte_length; int32_t binary; } NetworkMessageInfo; @@ -62,7 +62,7 @@ import_websg_networking(replicator_despawned_count) int32_t websg_network_replic typedef struct ReplicationInfo { node_id_t node_id; // Can be null when remote. Call factory if null. network_id_t network_id; // Can be null with local. - uint32_t peer_index; + uint64_t peer_index; uint32_t byte_length; } ReplicationInfo; diff --git a/src/engine/scripting/scripting.game.ts b/src/engine/scripting/scripting.game.ts index 561157cc7..fca607eda 100644 --- a/src/engine/scripting/scripting.game.ts +++ b/src/engine/scripting/scripting.game.ts @@ -10,6 +10,7 @@ import { createThirdroomModule } from "./thirdroom"; import { createWASIModule } from "./wasi"; import { WASMModuleContext } from "./WASMModuleContext"; import { createWebSGModule } from "./websg"; +import { PeerID } from "../network/network.game"; export enum ScriptState { Uninitialized, @@ -27,8 +28,8 @@ export interface Script { entered: () => void; update: (dt: number, time: number) => void; dispose: () => void; - peerEntered: (peerIndex: number) => void; - peerExited: (peerIndex: number) => void; + peerEntered: (peerIndex: PeerID) => void; + peerExited: (peerIndex: PeerID) => void; } export const ScriptComponent = new Map(); @@ -242,14 +243,14 @@ export async function loadScript( throw new Error("update() can only be called from the Entered state"); } }, - peerEntered(peerId: number) { + peerEntered(peerIndex: PeerID) { if (this.state === ScriptState.Error) { return; } if (this.state === ScriptState.Loaded || this.state === ScriptState.Entered) { if (websgPeerEntered) { - const result = websgPeerEntered(peerId); + const result = websgPeerEntered(peerIndex); if (result < 0) { console.error(`Script peerEntered callback failed with code: ${result}`); @@ -261,14 +262,14 @@ export async function loadScript( throw new Error("peerEntered() can only be called from the Loaded or Entered state"); } }, - peerExited(peerId: number) { + peerExited(peerIndex: PeerID) { if (this.state === ScriptState.Error) { return; } if (this.state === ScriptState.Loaded || this.state === ScriptState.Entered) { if (websgPeerExited) { - const result = websgPeerExited(peerId); + const result = websgPeerExited(peerIndex); if (result < 0) { console.error(`Script peerExited callback failed with code: ${result}`); @@ -281,7 +282,6 @@ export async function loadScript( } }, dispose() { - console.trace("script disposed"); disposeThirdroomModule(); disposeWebSGModule(); disposeMatrixModule(); diff --git a/src/engine/utils/Queue.ts b/src/engine/utils/Queue.ts new file mode 100644 index 000000000..401e59474 --- /dev/null +++ b/src/engine/utils/Queue.ts @@ -0,0 +1,21 @@ +export type Queue = Array & { + enqueue: (item: T) => number; + dequeue: () => T | undefined; +}; + +export function enqueue(queue: Queue, item: T): number { + return queue.push(item); +} + +export function dequeue(queue: Queue): T | undefined { + return queue.shift(); +} + +export function createQueue(): Queue { + const queue = [] as Array as Queue; + Object.defineProperties(queue, { + enqueue: { value: (item: T) => enqueue(queue, item) }, + dequeue: { value: () => dequeue(queue) }, + }); + return queue; +} diff --git a/src/plugins/interaction/interaction.game.ts b/src/plugins/interaction/interaction.game.ts index 2ea8f0cfb..3bc55c108 100644 --- a/src/plugins/interaction/interaction.game.ts +++ b/src/plugins/interaction/interaction.game.ts @@ -11,13 +11,8 @@ import { GameContext } from "../../engine/GameTypes"; import { enableActionMap } from "../../engine/input/ActionMappingSystem"; import { GameInputModule, InputModule } from "../../engine/input/input.game"; import { defineModule, getModule, Thread } from "../../engine/module/module.common"; -import { - GameNetworkState, - getPeerIndexFromNetworkId, - NetworkModule, - ownedNetworkedQuery, -} from "../../engine/network/network.game"; -import { Networked, Owned } from "../../engine/network/NetworkComponents"; +import { GameNetworkState, NetworkModule, authoringNetworkedQuery } from "../../engine/network/network.game"; +import { Networked, Authoring } from "../../engine/network/NetworkComponents"; import { takeOwnership } from "../../engine/network/ownership.game"; import { addCollisionGroupMembership, @@ -37,7 +32,6 @@ import { RemoteInteractable, RemoteNode, RemoteUIButton, - removeObjectFromWorld, } from "../../engine/resource/RemoteResources"; import { AudioEmitterType, InteractableType } from "../../engine/resource/schema"; import { PortalComponent } from "../portals/portals.game"; @@ -57,6 +51,7 @@ import { getCamera } from "../../engine/player/getCamera"; import { ThirdRoomMessageType } from "../thirdroom/thirdroom.common"; import { ThirdRoomModule, ThirdRoomModuleState } from "../thirdroom/thirdroom.game"; import { clamp } from "../../engine/common/math"; +import { tryGetNetworkReplicator } from "../../engine/network/NetworkReplicator"; // TODO: importing from spawnables.game in this file induces a runtime error // import { SpawnablesModule } from "../spawnables/spawnables.game"; @@ -336,11 +331,13 @@ export function InteractionSystem(ctx: GameContext) { updateGrabThrowXR(ctx, physics, network, input, thirdroom, rig, leftRay, "left"); updateGrabThrowXR(ctx, physics, network, input, thirdroom, rig, rightRay, "right"); } else { - const grabbingNode = getCamera(ctx, rig).parent!; + const grabbingNode = getCamera(ctx, rig)?.parent; - updateFocus(ctx, physics, rig, grabbingNode); - updateDeletion(ctx, interaction, input, eid); - updateGrabThrow(ctx, interaction, physics, network, input, thirdroom, rig, grabbingNode); + if (grabbingNode) { + updateFocus(ctx, physics, rig, grabbingNode); + updateDeletion(ctx, interaction, input, eid); + updateGrabThrow(ctx, interaction, physics, network, input, thirdroom, rig, grabbingNode); + } } } } @@ -464,7 +461,7 @@ function hitscan(physics: PhysicsModuleState, node: RemoteNode, collisionGroup: colliderShape, 10.0, true, - 0, + 0 as QueryFilterFlags, collisionGroup ); return hit; @@ -511,6 +508,7 @@ function updateFocus(ctx: GameContext, physics: PhysicsModuleState, rig: RemoteN } function updateDeletion(ctx: GameContext, interaction: InteractionModuleState, input: GameInputModule, rig: number) { + const network = getModule(ctx, NetworkModule); const deleteBtn = input.actionStates.get("Delete") as ButtonActionState; if (deleteBtn.pressed) { const focusedEid = FocusComponent.focusedEntity[rig]; @@ -518,10 +516,11 @@ function updateDeletion(ctx: GameContext, interaction: InteractionModuleState, i // TODO: For now we only delete owned objects if ( focused && - hasComponent(ctx.world, Owned, focused.eid) && + hasComponent(ctx.world, Authoring, focused.eid) && Interactable.type[focused.eid] === InteractableType.Grabbable ) { - removeObjectFromWorld(ctx, focused); + const replicator = tryGetNetworkReplicator(network, Networked.replicatorId[focusedEid]); + replicator.despawn(focused); playOneShotAudio(ctx, interaction.clickEmitter?.sources[1] as RemoteAudioSource, 0.4); } } @@ -575,7 +574,8 @@ function updateGrabThrow( _impulse.fromArray(direction); const heldNode = getRemoteResource(ctx, heldEntity); if (!heldNode || !heldNode.physicsBody || !heldNode.physicsBody.body) { - throw new Error(`No physics body found on entity ${heldEntity}`); + // entity was removed while held + return; } heldNode.physicsBody.body.applyImpulse(_impulse, true); @@ -615,8 +615,8 @@ function updateGrabThrow( if (grabPressed) { if (Interactable.type[node.eid] === InteractableType.Grabbable) { playOneShotAudio(ctx, interaction.clickEmitter?.sources[0] as RemoteAudioSource); - const ownedEnts = ownedNetworkedQuery(ctx.world); - if (ownedEnts.length > thirdroom.maxObjectCap && !hasComponent(ctx.world, Owned, node.eid)) { + const ownedEnts = authoringNetworkedQuery(ctx.world); + if (ownedEnts.length > thirdroom.maxObjectCap && !hasComponent(ctx.world, Authoring, node.eid)) { // do nothing if we hit the max obj cap ctx.sendMessage(Thread.Main, { type: ThirdRoomMessageType.ObjectCapReached, @@ -739,6 +739,7 @@ function updateGrabThrowXR( if (hit === null) { grabbingNode.visible = true; + return; } @@ -746,6 +747,7 @@ function updateGrabThrowXR( if (!focusedEntity) { grabbingNode.visible = true; + return; } @@ -763,8 +765,8 @@ function updateGrabThrowXR( if (hit.toi <= Interactable.interactionDistance[focusedNode.eid]) { if (squeezeState.held && Interactable.type[focusedNode.eid] === InteractableType.Grabbable) { - const ownedEnts = ownedNetworkedQuery(ctx.world); - if (ownedEnts.length > thirdroom.maxObjectCap && !hasComponent(ctx.world, Owned, focusedNode.eid)) { + const ownedEnts = authoringNetworkedQuery(ctx.world); + if (ownedEnts.length > thirdroom.maxObjectCap && !hasComponent(ctx.world, Authoring, focusedNode.eid)) { // do nothing if we hit the max obj cap // TODO: websgui // ctx.sendMessage(Thread.Main, { @@ -855,18 +857,17 @@ export function sendInteractionMessage(ctx: GameContext, action: InteractableAct action, }); } else { - let peerId; + let peerId: string | undefined; if (interactableType === InteractableType.Grabbable || interactableType === InteractableType.Player) { - peerId = network.entityIdToPeerId.get(eid); + peerId = network.entityIdToPeer.get(eid)?.key; } - let ownerId; + let ownerId: string | undefined; if (interactableType === InteractableType.Grabbable) { - const ownerIdIndex = getPeerIndexFromNetworkId(Networked.networkId[eid]); - ownerId = network.indexToPeerId.get(ownerIdIndex); - if (hasComponent(ctx.world, Owned, eid)) { + ownerId = network.peerIdToInfo.get(BigInt(Networked.authorId[eid]))?.key; + if (hasComponent(ctx.world, Authoring, eid)) { ownerId = peerId; } } diff --git a/src/plugins/spawnables/spawnables.game.ts b/src/plugins/spawnables/spawnables.game.ts index ba039052d..aee3f53ef 100644 --- a/src/plugins/spawnables/spawnables.game.ts +++ b/src/plugins/spawnables/spawnables.game.ts @@ -7,8 +7,8 @@ import { GameContext } from "../../engine/GameTypes"; import { createNodeFromGLTFURI } from "../../engine/gltf/gltf.game"; import { createSphereMesh } from "../../engine/mesh/mesh.game"; import { defineModule, getModule, Thread } from "../../engine/module/module.common"; -import { ownedNetworkedQuery } from "../../engine/network/network.game"; -import { Networked, Owned } from "../../engine/network/NetworkComponents"; +import { authoringNetworkedQuery, NetworkModule } from "../../engine/network/network.game"; +import { Authoring } from "../../engine/network/NetworkComponents"; import { dynamicObjectCollisionGroups } from "../../engine/physics/CollisionGroups"; import { addPhysicsBody, @@ -18,7 +18,6 @@ import { PhysicsModuleState, registerCollisionHandler, } from "../../engine/physics/physics.game"; -import { createPrefabEntity, PrefabType, registerPrefab } from "../../engine/prefab/prefab.game"; import { addObjectToWorld, RemoteAudioData, @@ -28,6 +27,7 @@ import { RemoteMaterial, RemoteNode, RemotePhysicsBody, + removeObjectFromWorld, } from "../../engine/resource/RemoteResources"; import { AudioEmitterType, @@ -42,11 +42,15 @@ import { addInteractableComponent } from "../interaction/interaction.game"; import { getRotationNoAlloc } from "../../engine/utils/getRotationNoAlloc"; import { ThirdRoomModule } from "../thirdroom/thirdroom.game"; import { ThirdRoomMessageType } from "../thirdroom/thirdroom.common"; +import { createNetworkReplicator, NetworkReplicator } from "../../engine/network/NetworkReplicator"; +import { transformCodec } from "../../engine/network/NetworkMessage"; +import { isHost } from "../../engine/network/network.common"; const { abs, floor, random } = Math; type SpawnablesModuleState = { hitAudioEmitters: Map; + replicators?: { [key: string]: NetworkReplicator }; }; export const SpawnablesModule = defineModule({ @@ -66,33 +70,6 @@ export const SpawnablesModule = defineModule }); crateAudioData.addRef(); - registerPrefab(ctx, { - name: "small-crate", - type: PrefabType.Object, - create: (ctx, { kinematic }) => { - const size = 1; - return createCrate(ctx, module, physics, size, crateAudioData, kinematic); - }, - }); - - registerPrefab(ctx, { - name: "medium-crate", - type: PrefabType.Object, - create: (ctx, { kinematic }) => { - const size = 1.75; - return createCrate(ctx, module, physics, size, crateAudioData, kinematic); - }, - }); - - registerPrefab(ctx, { - name: "large-crate", - type: PrefabType.Object, - create: (ctx, { kinematic }) => { - const size = 2.5; - return createCrate(ctx, module, physics, size, crateAudioData, kinematic); - }, - }); - const ballAudioData = new RemoteAudioData(ctx.resourceManager, { name: "Ball Audio Data", uri: "/audio/bounce.wav", @@ -139,37 +116,43 @@ export const SpawnablesModule = defineModule }); blackMirrorMaterial.addRef(); - registerPrefab(ctx, { - name: "small-ball", - type: PrefabType.Object, - create: (ctx, { kinematic }) => { - return createBall(ctx, module, physics, 0.25, emissiveMaterial, ballAudioData, kinematic); - }, - }); - - registerPrefab(ctx, { - name: "mirror-ball", - type: PrefabType.Object, - create: (ctx, { kinematic }) => { - return createBall(ctx, module, physics, 1, mirrorMaterial, ballAudioData, kinematic); - }, - }); - - registerPrefab(ctx, { - name: "black-mirror-ball", - type: PrefabType.Object, - create: (ctx, { kinematic }) => { - return createBall(ctx, module, physics, 1, blackMirrorMaterial, ballAudioData, kinematic); - }, - }); - - registerPrefab(ctx, { - name: "emissive-ball", - type: PrefabType.Object, - create: (ctx, { kinematic }) => { - return createBall(ctx, module, physics, 2, emissiveMaterial, ballAudioData, kinematic); - }, - }); + module.replicators = { + "small-crate": createNetworkReplicator( + ctx, + () => createCrate(ctx, module, physics, 1, crateAudioData), + transformCodec + ), + "medium-crate": createNetworkReplicator( + ctx, + () => createCrate(ctx, module, physics, 1.75, crateAudioData), + transformCodec + ), + "large-crate": createNetworkReplicator( + ctx, + () => createCrate(ctx, module, physics, 2.5, crateAudioData), + transformCodec + ), + "small-ball": createNetworkReplicator( + ctx, + () => createBall(ctx, module, physics, 0.25, emissiveMaterial, ballAudioData), + transformCodec + ), + "mirror-ball": createNetworkReplicator( + ctx, + () => createBall(ctx, module, physics, 1, mirrorMaterial, ballAudioData), + transformCodec + ), + "black-mirror-ball": createNetworkReplicator( + ctx, + () => createBall(ctx, module, physics, 1, blackMirrorMaterial, ballAudioData), + transformCodec + ), + "emissive-ball": createNetworkReplicator( + ctx, + () => createBall(ctx, module, physics, 2, emissiveMaterial, ballAudioData), + transformCodec + ), + }; // collision handlers const { physicsWorld } = getModule(ctx, PhysicsModule); @@ -323,10 +306,17 @@ const _spawnWorldQuat = quat.create(); // Returns false if the object exceeded the object cap export function spawnPrefab(ctx: GameContext, spawnFrom: RemoteNode, prefabId: string, isXR: boolean): boolean { + const network = getModule(ctx, NetworkModule); + if (!isHost(network)) { + // TODO: request spawn from the host + console.warn("Cannot spawn entity, this peer is not the host."); + return false; + } const { maxObjectCap } = getModule(ctx, ThirdRoomModule); + const { replicators } = getModule(ctx, SpawnablesModule); // bounce out of the function if we hit the max object cap - const ownedEnts = ownedNetworkedQuery(ctx.world); + const ownedEnts = authoringNetworkedQuery(ctx.world); if (ownedEnts.length > maxObjectCap) { ctx.sendMessage(Thread.Main, { type: ThirdRoomMessageType.ObjectCapReached, @@ -336,38 +326,60 @@ export function spawnPrefab(ctx: GameContext, spawnFrom: RemoteNode, prefabId: s return false; } - const prefab = createPrefabEntity(ctx, prefabId); - const eid = prefab.eid; + if (!replicators || !replicators[prefabId]) { + throw new Error("replicator not found"); + } - addComponent(ctx.world, Owned, eid); - addComponent(ctx.world, Networked, eid, true); + for (let i = 0; i < 10; i++) { + const node = replicators[prefabId].spawn(ctx); + const eid = node.eid; - mat4.getTranslation(prefab.position, spawnFrom.worldMatrix); + addComponent(ctx.world, Authoring, eid); - getRotationNoAlloc(_spawnWorldQuat, spawnFrom.worldMatrix); - const direction = vec3.set(_direction, 0, 0, -1); - vec3.transformQuat(direction, direction, _spawnWorldQuat); + mat4.getTranslation(node.position, spawnFrom.worldMatrix); - // place object at direction - vec3.add(prefab.position, prefab.position, direction); - prefab.quaternion.set(_spawnWorldQuat); + getRotationNoAlloc(_spawnWorldQuat, spawnFrom.worldMatrix); + const direction = vec3.set(_direction, 0, 0, -1 * i); + vec3.transformQuat(direction, direction, _spawnWorldQuat); - const body = prefab.physicsBody?.body; + // place object at direction + vec3.add(node.position, node.position, direction); + node.quaternion.set(_spawnWorldQuat); - if (!body) { - console.warn("could not find physics body for spawned entity " + eid); - return true; - } + const body = node.physicsBody?.body; - applyTransformToRigidBody(body, prefab); + if (!body) { + console.warn("could not find physics body for spawned entity " + eid); + return true; + } - const privateScene = ctx.worldResource.environment?.privateScene; + applyTransformToRigidBody(body, node); + } - if (!privateScene) { - throw new Error("private scene not found on environment"); + return true; +} + +const handleSpawnableReplicatorQueues = (ctx: GameContext, replicator: NetworkReplicator) => { + let spawn; + while ((spawn = replicator.spawned.dequeue())) { + const node = spawn.node; + addObjectToWorld(ctx, node); + } + let despawn; + while ((despawn = replicator.despawned.dequeue())) { + removeObjectFromWorld(ctx, despawn); } +}; - addObjectToWorld(ctx, prefab); +export function SpawnablesSystem(ctx: GameContext) { + const { replicators } = getModule(ctx, SpawnablesModule); - return true; + handleSpawnableReplicatorQueues(ctx, replicators!["small-crate"]); + handleSpawnableReplicatorQueues(ctx, replicators!["medium-crate"]); + handleSpawnableReplicatorQueues(ctx, replicators!["large-crate"]); + + handleSpawnableReplicatorQueues(ctx, replicators!["small-ball"]); + handleSpawnableReplicatorQueues(ctx, replicators!["mirror-ball"]); + handleSpawnableReplicatorQueues(ctx, replicators!["black-mirror-ball"]); + handleSpawnableReplicatorQueues(ctx, replicators!["emissive-ball"]); } diff --git a/src/plugins/thirdroom/action-bar.game.ts b/src/plugins/thirdroom/action-bar.game.ts index 5cfc5f31e..59c226650 100644 --- a/src/plugins/thirdroom/action-bar.game.ts +++ b/src/plugins/thirdroom/action-bar.game.ts @@ -4,7 +4,7 @@ import { ActionMap, ActionDefinition, ActionType, BindingType, ButtonActionState import { GameInputModule, InputModule } from "../../engine/input/input.game"; import { XRAvatarRig } from "../../engine/input/WebXRAvatarRigSystem"; import { getModule, Thread } from "../../engine/module/module.common"; -import { getCamera } from "../../engine/player/getCamera"; +import { tryGetCamera } from "../../engine/player/getCamera"; import { RemoteNode } from "../../engine/resource/RemoteResources"; import { tryGetRemoteResource } from "../../engine/resource/resource.game"; import { ScriptComponent, scriptQuery } from "../../engine/scripting/scripting.game"; @@ -128,7 +128,7 @@ export function ActionBarSystem(ctx: GameContext) { const rightRayNode = tryGetRemoteResource(ctx, xr.rightRayEid); return spawnPrefab(ctx, rightRayNode, actionBarItem.id, true); } else { - const camera = getCamera(ctx, node).parent; + const camera = tryGetCamera(ctx, node).parent; if (camera) { return spawnPrefab(ctx, camera, actionBarItem.id, true); diff --git a/src/plugins/thirdroom/thirdroom.common.ts b/src/plugins/thirdroom/thirdroom.common.ts index 8a6f205ab..035be16ad 100644 --- a/src/plugins/thirdroom/thirdroom.common.ts +++ b/src/plugins/thirdroom/thirdroom.common.ts @@ -23,6 +23,7 @@ export interface EnterWorldMessage { type: ThirdRoomMessageType.EnterWorld; id: number; localPeerId?: string; + hostPeerId?: string; } export interface EnteredWorldMessage { diff --git a/src/plugins/thirdroom/thirdroom.game.ts b/src/plugins/thirdroom/thirdroom.game.ts index 3152da39f..fa00cc17a 100644 --- a/src/plugins/thirdroom/thirdroom.game.ts +++ b/src/plugins/thirdroom/thirdroom.game.ts @@ -1,4 +1,4 @@ -import { defineQuery, hasComponent } from "bitecs"; +import { addComponent, defineQuery, hasComponent } from "bitecs"; import RAPIER from "@dimforge/rapier3d-compat"; import { SpawnPoint } from "../../engine/component/SpawnPoint"; @@ -12,7 +12,13 @@ import { registerMessageHandler, Thread, } from "../../engine/module/module.common"; -import { addPeerId, NetworkModule, removePeerId } from "../../engine/network/network.game"; +import { + addPeerId, + addPeerInfo, + GameNetworkState, + NetworkModule, + removePeerId, +} from "../../engine/network/network.game"; import { EnterWorldMessage, WorldLoadedMessage, @@ -41,7 +47,7 @@ import { registerCollisionHandler, } from "../../engine/physics/physics.game"; import { boundsCheckCollisionGroups } from "../../engine/physics/CollisionGroups"; -import { Player } from "../../engine/player/Player"; +import { OurPlayer, Player } from "../../engine/player/Player"; import { enableActionMap } from "../../engine/input/ActionMappingSystem"; import { InputModule } from "../../engine/input/input.game"; import { spawnEntity } from "../../engine/utils/spawnEntity"; @@ -69,13 +75,11 @@ import { findResourceRetainerRoots, findResourceRetainers } from "../../engine/r import { RemoteResource } from "../../engine/resource/RemoteResourceClass"; import { actionBarMap, setDefaultActionBarItems } from "./action-bar.game"; import { createDisposables } from "../../engine/utils/createDisposables"; -import { - registerPlayerPrefabs, - loadPlayerRig, - loadNetworkedPlayerRig, - spawnPlayer, -} from "../../engine/player/PlayerRig"; +import { registerPlayerPrefabs, teleportToSpawnPoint } from "../../engine/player/PlayerRig"; import { MAX_OBJECT_CAP } from "../../engine/config.common"; +import { NetworkReplicator } from "../../engine/network/NetworkReplicator"; +import { isHost } from "../../engine/network/network.common"; +import { Authoring, Networked } from "../../engine/network/NetworkComponents"; type WorldLoaderMessage = LoadWorldMessage | EnterWorldMessage | ExitWorldMessage | ReloadWorldMessage; @@ -95,6 +99,9 @@ export interface ThirdRoomModuleState { environmentScript?: Script; environmentGLTFResource?: GLTFResource; maxObjectCap: number; + replicators?: { + avatar: NetworkReplicator; + }; } const tempSpawnPoints: RemoteNode[] = []; @@ -131,8 +138,11 @@ export const ThirdRoomModule = defineModule({ }; }, async init(ctx) { - const { worldLoaderMessages } = getModule(ctx, ThirdRoomModule); + const thirdroom = getModule(ctx, ThirdRoomModule); + const { worldLoaderMessages } = thirdroom; + const input = getModule(ctx, InputModule); + const network = getModule(ctx, NetworkModule); const dispose = createDisposables([ worldLoaderMessages.register(ctx), @@ -146,7 +156,7 @@ export const ThirdRoomModule = defineModule({ console.error("Error loading avatar:", error); }); - registerPlayerPrefabs(ctx); + registerPlayerPrefabs(ctx, thirdroom); // create out of bounds floor check const physics = getModule(ctx, PhysicsModule); @@ -155,7 +165,7 @@ export const ThirdRoomModule = defineModule({ name: "Out of Bounds Floor", }); - oobFloor.position.set([size / 2, -150, size / 2]); + oobFloor.position.set([0, -150, 0]); addPhysicsCollider( ctx.world, @@ -190,11 +200,13 @@ export const ThirdRoomModule = defineModule({ const node = tryGetRemoteResource(ctx, objectEid); - if (hasComponent(ctx.world, Player, objectEid)) { - const spawnPoints = getSpawnPoints(ctx); - spawnEntity(spawnPoints, node); - } else { - removeObjectFromWorld(ctx, node); + if (isHost(network)) { + if (hasComponent(ctx.world, Player, objectEid)) { + const spawnPoints = getSpawnPoints(ctx); + spawnEntity(spawnPoints, node); + } else { + removeObjectFromWorld(ctx, node); + } } }); @@ -333,7 +345,15 @@ async function loadWorld(ctx: GameContext, environmentUrl: string, options: Load // when we join the world function onEnterWorld(ctx: GameContext, message: EnterWorldMessage) { try { - enterWorld(ctx, message.localPeerId); + console.log("onEnterWorld", message.localPeerId, message.hostPeerId); + + if (message.localPeerId && message.hostPeerId) { + const thirdroom = getModule(ctx, ThirdRoomModule); + const network = getModule(ctx, NetworkModule); + enterNetworkedWorld(ctx, thirdroom, network, message.localPeerId, message.hostPeerId); + } else { + enterViewerWorld(ctx); + } ctx.sendMessage(Thread.Main, { type: ThirdRoomMessageType.EnteredWorld, @@ -352,22 +372,49 @@ function onEnterWorld(ctx: GameContext, message: EnterWorldMessage) { } } -function enterWorld(ctx: GameContext, localPeerId?: string) { - const thirdroom = getModule(ctx, ThirdRoomModule); +async function onReloadWorld(ctx: GameContext, message: ReloadWorldMessage) { + try { + // TODO: probably don't need to do this on reload + disposeWorld(ctx); - if (thirdroom.loadState !== WorldLoadState.Loaded) { - throw new Error("Cannot enter world when world is not loaded."); - } + await loadWorld(ctx, message.environmentUrl, message.options); - const network = getModule(ctx, NetworkModule); - const physics = getModule(ctx, PhysicsModule); - const input = getModule(ctx, InputModule); - const { environmentScript, environmentGLTFResource } = getModule(ctx, ThirdRoomModule); + const thirdroom = getModule(ctx, ThirdRoomModule); + const network = getModule(ctx, NetworkModule); + enterNetworkedWorld(ctx, thirdroom, network, network.local!.key, network.host!.key); - if (!environmentGLTFResource) { - throw new Error("Cannot enter world: environment glTF resource not yet loaded."); + // reinform peers + for (const peerInfo of network.peers) { + removePeerId(ctx, peerInfo.key); + addPeerId(ctx, peerInfo.key); + } + + ctx.sendMessage(Thread.Main, { + type: ThirdRoomMessageType.ReloadedWorld, + id: message.id, + }); + } catch (error: any) { + disposeWorld(ctx); + + console.error(error); + + ctx.sendMessage(Thread.Main, { + type: ThirdRoomMessageType.ReloadWorldError, + id: message.id, + error: error.message || "Unknown error", + }); } +} +function onExitWorld(ctx: GameContext, message: ExitWorldMessage) { + disposeWorld(ctx); + + ctx.sendMessage(Thread.Main, { + type: ThirdRoomMessageType.ExitedWorld, + }); +} + +function setupEnvironment(ctx: GameContext, environmentGLTFResource: GLTFResource) { const environmentScene = loadDefaultGLTFScene(ctx, environmentGLTFResource, { createDefaultMeshColliders: true, rootIsStatic: true, @@ -406,16 +453,31 @@ function enterWorld(ctx: GameContext, localPeerId?: string) { privateScene: transientScene, }); - let rig: RemoteNode; + return environmentScene; +} + +function enterViewerWorld(ctx: GameContext) { + const thirdroom = getModule(ctx, ThirdRoomModule); - if (localPeerId) { - rig = loadNetworkedPlayerRig(ctx, physics, input, network, localPeerId); - } else { - rig = loadPlayerRig(ctx, physics, input); + if (thirdroom.loadState !== WorldLoadState.Loaded) { + throw new Error("Cannot enter world when world is not loaded."); + } + + if (!thirdroom.environmentGLTFResource) { + throw new Error("Cannot enter world: environment glTF resource not yet loaded."); } - spawnPlayer(ctx, rig); + const environmentScene = setupEnvironment(ctx, thirdroom.environmentGLTFResource); + + // TODO: use factory? + const rig = thirdroom.replicators!.avatar.spawn(ctx); + addComponent(ctx.world, Player, rig.eid); + addComponent(ctx.world, OurPlayer, rig.eid); + + teleportToSpawnPoint(ctx, rig); + + const { environmentScript } = thirdroom; if (environmentScript) { addScriptComponent(ctx, environmentScene, environmentScript); environmentScript.entered(); @@ -424,46 +486,53 @@ function enterWorld(ctx: GameContext, localPeerId?: string) { thirdroom.loadState = WorldLoadState.Entered; } -async function onReloadWorld(ctx: GameContext, message: ReloadWorldMessage) { - try { - const network = getModule(ctx, NetworkModule); +function enterNetworkedWorld( + ctx: GameContext, + thirdroom: ThirdRoomModuleState, + network: GameNetworkState, + localPeerKey: string, + hostPeerKey: string +) { + if (thirdroom.loadState !== WorldLoadState.Loaded) { + throw new Error("Cannot enter world when world is not loaded."); + } - // TODO: probably don't need to do this on reload - disposeWorld(ctx); + if (!thirdroom.environmentGLTFResource) { + throw new Error("Cannot enter world: environment glTF resource not yet loaded."); + } - await loadWorld(ctx, message.environmentUrl, message.options); + const environmentScene = setupEnvironment(ctx, thirdroom.environmentGLTFResource); - enterWorld(ctx, network.peerId); + // this is where the host spawns themself in + // other peers wait for their avatar to be spawned and authority transfered (handled by newPeersQueue on the host side) + if (localPeerKey === hostPeerKey) { + // create a new peer index and map it to our id + const peerId = network.peerIdCount++; + const peerInfo = addPeerInfo(network, localPeerKey, peerId); - // reinform peers - for (const peerId of network.peers) { - removePeerId(ctx, peerId); - addPeerId(ctx, peerId); - } + // this makes isHost(network) return true + network.local = peerInfo; + network.host = peerInfo; - ctx.sendMessage(Thread.Main, { - type: ThirdRoomMessageType.ReloadedWorld, - id: message.id, - }); - } catch (error: any) { - disposeWorld(ctx); + const rig = thirdroom.replicators!.avatar.spawn(ctx); - console.error(error); + addComponent(ctx.world, Authoring, rig.eid); + Networked.authorId[rig.eid] = Number(peerId); + rig.name = localPeerKey; - ctx.sendMessage(Thread.Main, { - type: ThirdRoomMessageType.ReloadWorldError, - id: message.id, - error: error.message || "Unknown error", - }); - } -} + addComponent(ctx.world, Player, rig.eid); + addComponent(ctx.world, OurPlayer, rig.eid); -function onExitWorld(ctx: GameContext, message: ExitWorldMessage) { - disposeWorld(ctx); + teleportToSpawnPoint(ctx, rig); - ctx.sendMessage(Thread.Main, { - type: ThirdRoomMessageType.ExitedWorld, - }); + const { environmentScript } = thirdroom; + if (environmentScript) { + addScriptComponent(ctx, environmentScene, environmentScript); + environmentScript.entered(); + } + } + + thirdroom.loadState = WorldLoadState.Entered; } function onSetObjectCap(ctx: GameContext, message: SetObjectCapMessage) { diff --git a/src/plugins/thirdroom/thirdroom.main.ts b/src/plugins/thirdroom/thirdroom.main.ts index 43470c158..c3c471d30 100644 --- a/src/plugins/thirdroom/thirdroom.main.ts +++ b/src/plugins/thirdroom/thirdroom.main.ts @@ -118,7 +118,7 @@ export async function loadWorld(ctx: MainContext, environmentUrl: string, option return loadingEnvironment.promise; } -export function enterWorld(ctx: MainContext, localPeerId: string) { +export function enterWorld(ctx: MainContext, localPeerId: string, hostPeerId: string) { const thirdroom = getModule(ctx, ThirdroomModule); const network = getModule(ctx, NetworkModule); const enteringWorld = createDeferred(false); @@ -148,12 +148,14 @@ export function enterWorld(ctx: MainContext, localPeerId: string) { registerMessageHandler(ctx, ThirdRoomMessageType.EnterWorldError, onEnterWorldError), ]); - network.peerId = localPeerId; + network.peerKey = localPeerId; + network.hostKey = hostPeerId; ctx.sendMessage(Thread.Game, { type: ThirdRoomMessageType.EnterWorld, id, localPeerId, + hostPeerId, }); return enteringWorld.promise; diff --git a/src/ui/hooks/useWorldLoader.ts b/src/ui/hooks/useWorldLoader.ts index e88953928..3c12c1810 100644 --- a/src/ui/hooks/useWorldLoader.ts +++ b/src/ui/hooks/useWorldLoader.ts @@ -10,7 +10,7 @@ import { registerMatrixNetworkInterface, provideMatrixNetworkInterface, } from "../../engine/network/createMatrixNetworkInterface"; -import { enterWorld, loadWorld, reloadWorld } from "../../plugins/thirdroom/thirdroom.main"; +import { loadWorld, reloadWorld } from "../../plugins/thirdroom/thirdroom.main"; import { worldAtom } from "../state/world"; import { useHydrogen } from "./useHydrogen"; import { useMainThreadContext } from "./useMainThread"; @@ -77,7 +77,7 @@ export function useWorldLoader(): WorldLoader { registerMatrixNetworkInterface(matrixNetworkInterface); - await enterWorld(mainThread, session.userId); + // await enterWorld(mainThread, session.userId, hostId); const audio = getModule(mainThread, AudioModule); audio.context.resume().catch(() => console.error("Couldn't resume audio context")); diff --git a/test/engine/mocks.ts b/test/engine/mocks.ts index ef3f60584..d6d45d118 100644 --- a/test/engine/mocks.ts +++ b/test/engine/mocks.ts @@ -11,6 +11,7 @@ import { RemoteNode, RemoteScene, RemoteWorld, RemoteEnvironment } from "../../s import { addChild } from "../../src/engine/component/transform"; import { MatrixModule } from "../../src/engine/matrix/matrix.game"; import { WebSGNetworkModule } from "../../src/engine/network/scripting.game"; +import { XRMode } from "../../src/engine/renderer/renderer.common"; export function registerDefaultPrefabs(ctx: GameContext) { registerPrefab(ctx, { @@ -68,9 +69,23 @@ export const mockRenderState = () => ({ meshPrimitives: [], }); +const peerInfo = { + key: "a", + id: 1, + lastUpdate: 0, + xrMode: XRMode.None, +}; + export const mockNetworkState = () => ({ networkIdToEntityId: new Map(), - prefabToReplicator: new Map(), + peerIndexCount: 1, + peerIdToInfo: new Map(), + peerKeyToInfo: new Map(), + replicators: new Map(), + replicatorIdCount: 1, + host: peerInfo, + local: peerInfo, + peers: [], }); export const mockResourceModule = () => ({ diff --git a/test/engine/network/index.test.ts b/test/engine/network/index.test.ts index c36f93894..c0ba87db1 100644 --- a/test/engine/network/index.test.ts +++ b/test/engine/network/index.test.ts @@ -1,31 +1,23 @@ -// import { describe, it } from "vitest"; import { ok, strictEqual } from "assert"; import { addComponent, entityExists, getEntityComponents, removeComponent } from "bitecs"; -import { GameContext } from "../../../src/engine/GameTypes"; -import { - createNetworkId, - getPeerIndexFromNetworkId, - getLocalIdFromNetworkId, - remoteNetworkedQuery, - ownedNetworkedQuery, - NetworkModule, -} from "../../../src/engine/network/network.game"; -import { Owned, Networked } from "../../../src/engine/network/NetworkComponents"; +import { remoteNetworkedQuery, authoringNetworkedQuery, NetworkModule } from "../../../src/engine/network/network.game"; +import { Authoring, Networked } from "../../../src/engine/network/NetworkComponents"; import { createCursorView, readFloat32, readString, readUint16, readUint32, + readUint64, skipUint32, } from "../../../src/engine/allocator/CursorView"; import { mockGameState } from "../mocks"; import { getModule } from "../../../src/engine/module/module.common"; import { addPrefabComponent } from "../../../src/engine/prefab/prefab.game"; import { - serializeTransformSnapshot, - deserializeTransformSnapshot, + writeTransform, + readTransform, serializeTransformChanged, deserializeTransformChanged, serializeUpdatesSnapshot, @@ -49,30 +41,30 @@ const clearComponentData = () => { new Uint8Array(Networked.quaternion[0].buffer).fill(0); }; -describe("Network Tests", () => { - describe("networkId", () => { - it("should #getPeerIdFromNetworkId()", () => { - const nid = 0xfff0_000f; - strictEqual(getPeerIndexFromNetworkId(nid), 0x000f); - }); - it("should #getLocalIdFromNetworkId()", () => { - const nid = 0xfff0_000f; - strictEqual(getLocalIdFromNetworkId(nid), 0xfff0); - }); - // hack - remove for id layer - it.skip("should #createNetworkId", () => { - const state = { - network: { - peerId: "abc", - peerIdToIndex: new Map([["abc", 0x00ff]]), - localIdCount: 0x000f, - removedLocalIds: [], - }, - } as unknown as GameContext; - const nid = createNetworkId(state); - strictEqual(nid, 0x000f_00ff); - }); - }); +describe.skip("Network Tests", () => { + // describe("networkId", () => { + // it("should #getPeerIdFromNetworkId()", () => { + // const nid = 0xfff0_000f; + // strictEqual(getPeerIndexFromNetworkId(nid), 0x000f); + // }); + // it("should #getLocalIdFromNetworkId()", () => { + // const nid = 0xfff0_000f; + // strictEqual(getLocalIdFromNetworkId(nid), 0xfff0); + // }); + // // hack - remove for id layer + // it.skip("should #createNetworkId", () => { + // const state = { + // network: { + // peerId: "abc", + // peerIdToIndex: new Map([["abc", 0x00ff]]), + // localIdCount: 0x000f, + // removedLocalIds: [], + // }, + // } as unknown as GameContext; + // const nid = createNetworkId(state); + // strictEqual(nid, 0x000f_00ff); + // }); + // }); describe("transform serialization", () => { beforeEach(clearComponentData); it("should #serializeTransformSnapshot()", () => { @@ -96,7 +88,7 @@ describe("Network Tests", () => { const velocity = node.physicsBody!.velocity; velocity.set([4, 5, 6]); - serializeTransformSnapshot(writer, node); + writeTransform(writer, node); const reader = createCursorView(writer.buffer); @@ -154,7 +146,7 @@ describe("Network Tests", () => { const quaternion = node.quaternion; velocity.set([4, 5, 6]); - serializeTransformSnapshot(writer, node); + writeTransform(writer, node); position.set([0, 0, 0]); velocity.set([0, 0, 0]); @@ -162,7 +154,7 @@ describe("Network Tests", () => { const reader = createCursorView(writer.buffer); - deserializeTransformSnapshot(network, reader, 0, node); + readTransform(network, reader, 0n, node); strictEqual(Networked.position[eid][0], 1); strictEqual(Networked.position[eid][1], 2); @@ -300,7 +292,7 @@ describe("Network Tests", () => { const reader = createCursorView(writer.buffer); - deserializeTransformChanged(reader, eid, node); + deserializeTransformChanged(reader, BigInt(eid), node); strictEqual(Networked.position[eid][0], 1); strictEqual(Networked.position[eid][1], 2); @@ -338,7 +330,7 @@ describe("Network Tests", () => { const reader = createCursorView(writer.buffer); - deserializeTransformChanged(reader, eid, node); + deserializeTransformChanged(reader, BigInt(eid), node); strictEqual(Networked.position[eid][0], 0); strictEqual(Networked.position[eid][1], 2); @@ -377,7 +369,7 @@ describe("Network Tests", () => { node.physicsBody?.velocity.set([1, 2, 3]); addComponent(state.world, Networked, eid); Networked.networkId[eid] = eid; - addComponent(state.world, Owned, eid); + addComponent(state.world, Authoring, eid); return node; }); @@ -389,8 +381,8 @@ describe("Network Tests", () => { strictEqual(count, 3); nodes.forEach((node) => { - const nid = Networked.networkId[node.eid]; - strictEqual(nid, readUint32(reader)); + const nid = BigInt(Networked.networkId[node.eid]); + strictEqual(nid, readUint64(reader)); const position = node.position; strictEqual(position[0], readFloat32(reader)); @@ -437,8 +429,8 @@ describe("Network Tests", () => { node.quaternion.set([4, 5, 6, 7]); addComponent(state.world, Networked, eid); Networked.networkId[eid] = eid; - network.networkIdToEntityId.set(eid, eid); - addComponent(state.world, Owned, eid); + network.networkIdToEntityId.set(BigInt(eid), eid); + addComponent(state.world, Authoring, eid); return node; }); @@ -491,7 +483,7 @@ describe("Network Tests", () => { node.quaternion.set([4, 5, 6, 7]); addComponent(state.world, Networked, eid); Networked.networkId[eid] = eid; - addComponent(state.world, Owned, eid); + addComponent(state.world, Authoring, eid); node.skipLerp = 0; return node; }); @@ -504,8 +496,8 @@ describe("Network Tests", () => { strictEqual(count, 3); nodes.forEach((node) => { - const nid = Networked.networkId[node.eid]; - strictEqual(nid, readUint32(reader)); + const nid = BigInt(Networked.networkId[node.eid]); + strictEqual(nid, readUint64(reader)); const changeMask = readUint16(reader); strictEqual(changeMask, 0b1111000111, `Expected ${toBinaryString(changeMask)} to equal 0b1111000111`); @@ -547,8 +539,8 @@ describe("Network Tests", () => { node.quaternion.set([4, 5, 6, 7]); addComponent(state.world, Networked, eid); Networked.networkId[eid] = eid; - network.networkIdToEntityId.set(eid, eid); - addComponent(state.world, Owned, eid); + network.networkIdToEntityId.set(BigInt(eid), eid); + addComponent(state.world, Authoring, eid); return node; }); @@ -585,11 +577,11 @@ describe("Network Tests", () => { ents.forEach(({ eid }) => { addComponent(state.world, Networked, eid); Networked.networkId[eid] = eid; - addComponent(state.world, Owned, eid); + addComponent(state.world, Authoring, eid); addPrefabComponent(state.world, eid, "test-prefab"); }); - strictEqual(ownedNetworkedQuery(state.world).length, 3); + strictEqual(authoringNetworkedQuery(state.world).length, 3); serializeCreates(state, writer); @@ -599,7 +591,7 @@ describe("Network Tests", () => { strictEqual(count, 3); ents.forEach(({ eid }) => { - strictEqual(readUint32(reader), eid); + strictEqual(readUint64(reader), BigInt(eid)); strictEqual(readString(reader), "test-prefab"); skipUint32(reader); // Data length }); @@ -618,13 +610,13 @@ describe("Network Tests", () => { }); ents.forEach(({ eid }) => { - addComponent(state.world, Owned, eid); + addComponent(state.world, Authoring, eid); addComponent(state.world, Networked, eid); Networked.networkId[eid] = eid; addPrefabComponent(state.world, eid, "test-prefab"); }); - const localEntities = ownedNetworkedQuery(state.world); + const localEntities = authoringNetworkedQuery(state.world); strictEqual(localEntities.length, 3); strictEqual(remoteNetworkedQuery(state.world).length, 0); @@ -645,7 +637,7 @@ describe("Network Tests", () => { ok(incomingEid !== outgoingEid); strictEqual(Networked.networkId[incomingEid], outgoingEid); - strictEqual(network.networkIdToEntityId.get(outgoingEid), incomingEid); + strictEqual(network.networkIdToEntityId.get(BigInt(outgoingEid)), incomingEid); } }); }); @@ -664,11 +656,11 @@ describe("Network Tests", () => { ents.forEach(({ eid }) => { addComponent(state.world, Networked, eid); Networked.networkId[eid] = eid; - addComponent(state.world, Owned, eid); + addComponent(state.world, Authoring, eid); addPrefabComponent(state.world, eid, "test-prefab"); }); - strictEqual(ownedNetworkedQuery(state.world).length, 3); + strictEqual(authoringNetworkedQuery(state.world).length, 3); ents.forEach(({ eid }) => { // todo: default removeComponent to not clear component data @@ -683,7 +675,7 @@ describe("Network Tests", () => { strictEqual(count, 3); ents.forEach(({ eid }) => { - strictEqual(readUint32(reader), eid); + strictEqual(readUint64(reader), BigInt(eid)); }); }); it("should #deserializeDeletes()", () => { @@ -700,11 +692,11 @@ describe("Network Tests", () => { ents.forEach(({ eid }) => { addComponent(state.world, Networked, eid); Networked.networkId[eid] = eid; - addComponent(state.world, Owned, eid); + addComponent(state.world, Authoring, eid); addPrefabComponent(state.world, eid, "test-prefab"); }); - strictEqual(ownedNetworkedQuery(state.world).length, 3); + strictEqual(authoringNetworkedQuery(state.world).length, 3); serializeCreates(state, writer); @@ -719,7 +711,7 @@ describe("Network Tests", () => { removeComponent(state.world, Networked, node.eid, false); }); - strictEqual(ownedNetworkedQuery(state.world).length, 0); + strictEqual(authoringNetworkedQuery(state.world).length, 0); // todo: make queue // strictEqual(deletedOwnedNetworkedQuery(state.world).length, 3); @@ -728,7 +720,7 @@ describe("Network Tests", () => { serializeDeletes(state, writer2); - remoteEntities.forEach((eid) => { + remoteEntities.forEach((eid: number) => { ok(entityExists(state.world, eid)); }); @@ -736,7 +728,7 @@ describe("Network Tests", () => { deserializeDeletes(state, reader2); - remoteEntities.forEach((eid) => { + remoteEntities.forEach((eid: number) => { ok(getEntityComponents(state.world, eid).length === 0); }); });