mirror of
https://github.com/node-red/node-red.git
synced 2025-03-01 10:36:34 +00:00
218 lines
8.0 KiB
JavaScript
218 lines
8.0 KiB
JavaScript
|
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 = $('<li class="red-ui-multiplayer-user"><button type="button" class="red-ui-multiplayer-user-icon" href="#"></button></li>')
|
||
|
.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 = $('<div>')
|
||
|
$('<div style="text-align: center">').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) {
|
||
|
$('<div>').text(`${ws.type}: ${ws.label||ws.name||ws.id}`).appendTo(content)
|
||
|
} else {
|
||
|
$('<div>').text(`tab: unknown`).appendTo(content)
|
||
|
}
|
||
|
}
|
||
|
if (location.node) {
|
||
|
const node = RED.nodes.node(location.node)
|
||
|
if (node) {
|
||
|
$('<div>').text(`node: ${node.id}`).appendTo(content)
|
||
|
} else {
|
||
|
$('<div>').text(`node: unknown`).appendTo(content)
|
||
|
}
|
||
|
}
|
||
|
return content
|
||
|
},
|
||
|
}).open()
|
||
|
})
|
||
|
if (!user.user.image) {
|
||
|
$('<i class="fa fa-user"></i>').appendTo(button);
|
||
|
} else {
|
||
|
$('<span class="user-profile"></span>').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 = $('<li><ul id="red-ui-multiplayer-user-list"></ul></li>').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)
|
||
|
}
|
||
|
}
|
||
|
})();
|