RED.multiplayer = (function () { // sessionId - used to identify sessions across websocket reconnects let sessionId let headerWidget // Map of session id to { session:'', user:{}, location:{}} let connections = {} // Map of username to { user:{}, connections:[] } let users = {} function addUserConnection (connection) { if (connections[connection.session]) { // This is an existing connection that has been authenticated const existingConnection = connections[connection.session] if (existingConnection.user.username !== connection.user.username) { removeUserButton(users[existingConnection.user.username]) } } connections[connection.session] = connection const user = users[connection.user.username] = users[connection.user.username] || { user: connection.user, connections: [] } connection.location = connection.location || {} user.connections.push(connection) if (connection.user.username === RED.settings.user?.username || connection.session === sessionId ) { // This is the current user - do not add a extra button for them } else { if (user.connections.length === 1) { if (user.button) { clearTimeout(user.inactiveTimeout) clearTimeout(user.removeTimeout) user.button.removeClass('inactive') } else { addUserButton(user) } } } } function removeUserConnection (session, isDisconnected) { const connection = connections[session] delete connections[session] const user = users[connection.user.username] const i = user.connections.indexOf(connection) user.connections.splice(i, 1) if (isDisconnected) { removeUserButton(user) } else { if (user.connections.length === 0) { // Give the user 5s to reconnect before marking inactive user.inactiveTimeout = setTimeout(() => { user.button.addClass('inactive') // Give the user further 20 seconds to reconnect before removing them // from the user toolbar entirely user.removeTimeout = setTimeout(() => { removeUserButton(user) }, 20000) }, 5000) } } } function addUserButton (user) { user.button = $('
  • ') .attr('data-username', user.user.username) .prependTo("#red-ui-multiplayer-user-list"); var button = user.button.find("button") button.on('click', function () { RED.popover.create({ target:button, trigger: 'modal', interactive: true, width: "250px", direction: 'bottom', content: () => { const content = $('
    ') $('
    ').text(user.user.username).appendTo(content) const location = user.connections[0].location if (location.workspace) { const ws = RED.nodes.workspace(location.workspace) || RED.nodes.subflow(location.workspace) if (ws) { $('
    ').text(`${ws.type}: ${ws.label||ws.name||ws.id}`).appendTo(content) } else { $('
    ').text(`tab: unknown`).appendTo(content) } } if (location.node) { const node = RED.nodes.node(location.node) if (node) { $('
    ').text(`node: ${node.id}`).appendTo(content) } else { $('
    ').text(`node: unknown`).appendTo(content) } } return content }, }).open() }) if (!user.user.image) { $('').appendTo(button); } else { $('').css({ backgroundImage: "url("+user.user.image+")", }).appendTo(button); } } function getLocation () { const location = { workspace: RED.workspaces.active() } const editStack = RED.editor.getEditStack() for (let i = editStack.length - 1; i >= 0; i--) { if (editStack[i].id) { location.node = editStack[i].id break } } return location } function updateLocation () { const location = getLocation() if (location.workspace !== 0) { log('send', 'multiplayer/location', location) RED.comms.send('multiplayer/location', location) } } function removeUserButton (user) { user.button.remove() delete user.button } function updateUserLocation (data) { connections[data.session].location = data delete data.session } return { init: function () { sessionId = RED.settings.getLocal('multiplayer:sessionId') if (!sessionId) { sessionId = RED.nodes.id() RED.settings.setLocal('multiplayer:sessionId', sessionId) } headerWidget = $('
    • ').prependTo('.red-ui-header-toolbar') RED.comms.on('connect', () => { const location = getLocation() const connectInfo = { session: sessionId } if (location.workspace !== 0) { connectInfo.location = location } RED.comms.send('multiplayer/connect', connectInfo) }) RED.comms.subscribe('multiplayer/#', (topic, msg) => { log('recv', topic, msg) if (topic === 'multiplayer/init') { // We have just reconnected, runtime has sent state to // initialise the world connections = {} users = {} $('#red-ui-multiplayer-user-list').empty() msg.forEach(connection => { addUserConnection(connection) }) } else if (topic === 'multiplayer/connection-added') { addUserConnection(msg) } else if (topic === 'multiplayer/connection-removed') { removeUserConnection(msg.session, msg.disconnected) } else if (topic === 'multiplayer/location') { updateUserLocation(msg) } }) RED.events.on('workspace:change', (event) => { updateLocation() }) RED.events.on('editor:open', () => { updateLocation() }) RED.events.on('editor:close', () => { updateLocation() }) RED.events.on('editor:change', () => { updateLocation() }) RED.events.on('login', () => { updateLocation() }) RED.events.on('logout', () => { const disconnectInfo = { session: sessionId } RED.comms.send('multiplayer/disconnect', disconnectInfo) RED.settings.removeLocal('multiplayer:sessionId') }) } } function log() { if (RED.multiplayer.DEBUG) { console.log('[multiplayer]', ...arguments) } } })();