import { addComponent , defineComponent , defineQuery , Types , Not , exitQuery , enterQuery , addEntity , } from "bitecs" ; import { GroupCall } from "@robertlong/matrix-js-sdk/lib/webrtc/groupCall" ; import { GroupCallParticipant } from "@robertlong/matrix-js-sdk/lib/webrtc/groupCallParticipant" ; import { World } from "./World" ; export enum NetworkMessageType { Create = 0 , Update = 1 , Delete = 2 } export interface NetworkMessage { type : NetworkMessageType ; sender : GroupCallParticipant ; networkId : number ; templateId ? : number ; lastOwned ? : number ; data ? : any ; } export const Networked = defineComponent ( { networkId : Types . ui32 , templateId : Types . ui32 , ownerId : Types . ui32 , lastOwned : Types . ui32 , sent : Types . ui8 , } ) ; const updatedNetworkedQuery = defineQuery ( [ Networked ] ) ; const createdNetworkedQuery = enterQuery ( updatedNetworkedQuery ) ; const deletedNetworkedQuery = exitQuery ( updatedNetworkedQuery ) ; export function isMine ( world , eid ) { return Networked . ownerId [ eid ] === world . localParticipantId ; } export function isMessageSenderOwner ( message , world , eid ) { if ( message . lastOwned ! > Networked . lastOwned [ eid ] ) { return true ; } const localOwnerId = Networked . ownerId [ eid ] ; const localOwner = world . participantIdToParticipant . get ( localOwnerId ) ! ; const senderMxId = message . sender . member . userId ; return ( message . lastOwned === Networked . lastOwned [ eid ] && senderMxId > localOwner . member . userId ) ; } export function takeOwnership ( world , eid ) { Networked . ownerId [ eid ] = world . localParticipantId ; Networked . lastOwned [ eid ] = performance . now ( ) ; } export function getNetworkTemplateId ( world , networkTemplate ) { return world . networkTemplateIds . get ( networkTemplate ) ; } export function registerNetworkTemplate ( world , networkTemplate ) { const networkTemplateId = world . networkTemplates . length ; world . networkTemplateIds . set ( networkTemplate , networkTemplateId ) ; world . networkTemplates . push ( networkTemplate ) ; return networkTemplateId ; } const MAX_NETWORKED_OBJECTS = 1024 ; let nextNetworkIdIndex = 0 ; const networkIds = crypto . getRandomValues ( new Uint32Array ( MAX_NETWORKED_OBJECTS ) ) ; export function createNetworkId ( ) { return networkIds [ nextNetworkIdIndex ++ ] ; } export function addNetworkedComponent ( world , eid , templateId ) { addComponent ( world , Networked , eid ) ; Networked . templateId [ eid ] = templateId ; Networked . networkId [ eid ] = createNetworkId ( ) ; Networked . lastOwned [ eid ] = performance . now ( ) ; Networked . ownerId [ eid ] = world . localParticipantId ; } interface ParticipantChannel { datachannel : RTCDataChannel ; firstSync : boolean ; } export interface NetworkTemplate { onCreate : ( world : World , sender : GroupCallParticipant , entityId : number , networkId : number , data : any ) => void ; onUpdate : ( world : World , sender : GroupCallParticipant , entityId : number , networkId : number , data : any ) => void ; onDelete : ( world : World , sender : GroupCallParticipant , entityId : number , networkId : number ) => void ; sendCreate : ( world : World , entityId : number , networkId : number ) => any ; sendUpdate : ( world : World , entityId : number , networkId : number ) => any ; } let nextParticipantId = 0 ; export function NetworkingModule ( world , { groupCall , networkTickInterval , } ) { const participantChannels = [ ] ; const inboundNetworkMessages = [ ] ; let lastNetworkTick = 0 ; world . networkTemplates = [ ] ; world . networkTemplateIds = new Map ( ) ; world . networkIdToEntity = new Map ( ) ; world . localParticipantId = nextParticipantId ++ ; world . participantToParticipantId = new Map ( [ [ groupCall . localParticipant , world . localParticipantId ] , ] ) ; world . participantIdToParticipant = new Map ( [ [ world . localParticipantId , groupCall . localParticipant ] , ] ) ; const onDatachannel = ( datachannel , sender ) => { let participantChannel ; let participantId ; function onDatachannelOpen ( ) { participantChannel = { datachannel , firstSync : true } ; participantChannels . push ( participantChannel ) ; participantId = nextParticipantId ++ ; world . participantToParticipantId . set ( sender , participantId ) ; world . participantIdToParticipant . set ( participantId , sender ) ; } if ( datachannel . readyState === "open" ) { onDatachannelOpen ( ) ; } else { datachannel . addEventListener ( "open" , onDatachannelOpen ) ; } datachannel . addEventListener ( "message" , ( event ) => { const { t : type , n : networkId , x : templateId , o : lastOwned , d : data , } = JSON . parse ( event . data ) ; inboundNetworkMessages . push ( { type , sender , networkId , templateId , data , lastOwned , } ) ; } ) ; datachannel . addEventListener ( "close" , ( ) => { const index = participantChannels . indexOf ( participantChannel ) ; if ( index !== - 1 ) { participantChannels . splice ( index , 1 ) ; } world . participantToParticipantId . delete ( sender ) ; world . participantIdToParticipant . delete ( participantId ) ; } ) ; } ; groupCall . on ( "datachannel" , onDatachannel ) ; const ReceiveMessagesSystem = ( world ) => { while ( inboundNetworkMessages . length ) { const message = inboundNetworkMessages . pop ( ) as NetworkMessage ; if ( message . type === NetworkMessageType . Create ) { const eid = addEntity ( world ) ; addComponent ( world , Networked , eid ) ; Networked . templateId [ eid ] = message . templateId ! ; Networked . networkId [ eid ] = message . networkId ; Networked . lastOwned [ eid ] = message . lastOwned ! ; Networked . ownerId [ eid ] = world . participantToParticipantId . get ( message . sender ) ! ; const template = world . networkTemplates [ message . templateId ! ] ; world . networkIdToEntity . set ( message . networkId , eid ) ; template . onCreate ( world , message . sender , eid , message . networkId , message . data ) ; } else if ( message . type === NetworkMessageType . Update ) { const entityId = world . networkIdToEntity . get ( message . networkId ) ! ; const templateId = Networked . templateId [ entityId ] ; const template = world . networkTemplates [ templateId ] ; if ( ! template ) { continue ; } template . onUpdate ( world , message . sender , entityId , message . networkId , message . data ) ; } else if ( message . type === NetworkMessageType . Delete ) { const entityId = world . networkIdToEntity . get ( message . networkId ) ! ; const templateId = Networked . templateId [ entityId ] ; const template = world . networkTemplates [ templateId ] ; template . onDelete ( world , message . sender , entityId , message . networkId ) ; } } return world ; } ; function broadcastCreateMessage ( networkId , lastOwned , templateId , data ) { const createPayload = JSON . stringify ( { t : NetworkMessageType . Create , n : networkId , x : templateId , o : lastOwned , d : data , } ) ; for ( let i = 0 ; i < participantChannels . length ; i ++ ) { participantChannels [ i ] . datachannel . send ( createPayload ) ; } } function broadcastUpdateMessage ( networkId , lastOwned , templateId , data ) { const createPayload = JSON . stringify ( { t : NetworkMessageType . Create , n : networkId , x : templateId , o : lastOwned , d : data , } ) ; const updatePayload = JSON . stringify ( { t : NetworkMessageType . Update , n : networkId , o : lastOwned , d : data , } ) ; for ( let i = 0 ; i < participantChannels . length ; i ++ ) { const participantChannel = participantChannels [ i ] ; let payload ; if ( participantChannel . firstSync ) { payload = createPayload ; } else { payload = updatePayload ; } participantChannel . datachannel . send ( payload ) ; } } function broadcastDeleteMessage ( networkId ) { const deletePayload = JSON . stringify ( { t : NetworkMessageType . Delete , n : networkId , } ) ; for ( let i = 0 ; i < participantChannels . length ; i ++ ) { participantChannels [ i ] . datachannel . send ( deletePayload ) ; } } const SendMessagesSystem = ( world ) => { const now = performance . now ( ) ; if ( now - lastNetworkTick >= networkTickInterval ) { const createdEntities = createdNetworkedQuery ( world ) ; const updatedEntities = updatedNetworkedQuery ( world ) ; const deletedEntities = deletedNetworkedQuery ( world ) ; for ( let i = 0 ; i < createdEntities . length ; i ++ ) { const entityId = createdEntities [ i ] ; if ( ! isMine ( world , entityId ) ) { continue ; } const networkId = Networked . networkId [ entityId ] ; const templateId = Networked . templateId [ entityId ] ; const lastOwned = Networked . lastOwned [ entityId ] ; const template = world . networkTemplates [ templateId ] ; const data = template . sendCreate ( world , entityId , networkId ) ; broadcastCreateMessage ( networkId , lastOwned , templateId , data ) ; } for ( let i = 0 ; i < updatedEntities . length ; i ++ ) { const entityId = updatedEntities [ i ] ; if ( ! isMine ( world , entityId ) ) { continue ; } const networkId = Networked . networkId [ entityId ] ; const templateId = Networked . templateId [ entityId ] ; const lastOwned = Networked . lastOwned [ entityId ] ; const template = world . networkTemplates [ templateId ] ; const data = template . sendUpdate ( world , entityId , networkId ) ; broadcastUpdateMessage ( networkId , lastOwned , templateId , data ) ; } for ( let i = 0 ; i < deletedEntities . length ; i ++ ) { const entityId = deletedEntities [ i ] ; if ( ! isMine ( world , entityId ) ) { continue ; } const networkId = Networked . networkId [ entityId ] ; broadcastDeleteMessage ( networkId ) ; } for ( let i = 0 ; i < participantChannels . length ; i ++ ) { participantChannels [ i ] . firstSync = false ; } lastNetworkTick = now ; } return world ; } ; const dispose = ( ) => { for ( const { datachannel } of participantChannels ) { datachannel . close ( ) ; } groupCall . removeListener ( "datachannel" , onDatachannel ) ; } ; return { ReceiveMessagesSystem , SendMessagesSystem , dispose } ; }