cables_dev/cables_ui/src/ui/multiplayer/sc_state.js
- import { Logger, Events } from "cables-shared-client";
- import Gui from "../gui.js";
- import ScClient from "./sc_client.js";
-
- CABLES = CABLES || {};
-
- export default class ScState extends Events
- {
- constructor(connection)
- {
- super();
-
- this.PILOT_REQUEST_TIMEOUT = 20000;
-
- this._log = new Logger("scstate");
-
- this._connection = connection;
-
- this._clients = {};
- this._clients[connection.clientId] = new ScClient({
- "username": gui.user.username,
- "userid": gui.user.id,
- "clientId": connection.clientId,
- "isMe": true,
- "isRemoteClient": gui.isRemoteClient,
- "multiplayerCapable": this._connection.multiplayerCapable,
- "isPilot": false
- });
- this._followers = [];
- this._colors = {};
- this._pilot = null;
- this._timeoutRefresh = null;
-
- this._registerEventListeners();
- }
-
- get clients() { return this._clients; }
-
- get followers() { return this._followers; }
-
-
- getUserId(clientId)
- {
- if (this._clients[clientId])
- return this._clients[clientId].userid;
- }
-
-
- getUserInSubpatch(subPatch)
- {
- const userIds = [];
- for (const i in this._clients)
- {
- if (!this._clients[i].isMe && this._clients[i].subpatch == subPatch)
- userIds.push(this._clients[i].userid);
- }
-
- return userIds;
- }
-
- _onPingAnswer(payload)
- {
- let userListChanged = false;
- if (payload.isDisconnected)
- {
- if (this._clients[payload.clientId])
- {
- const wasInMultiplayerSession = this._clients[payload.clientId].inMultiplayerSession;
- if (this._connection.clientId !== payload.clientId)
- {
- delete this._clients[payload.clientId];
- this.emitEvent("clientDisconnected", payload, wasInMultiplayerSession);
- userListChanged = true;
- }
- }
- }
- else
- {
- const client = new ScClient(payload, this._connection.client);
- if (this._clients[payload.clientId])
- {
- if (!payload.inMultiplayerSession && this._clients[payload.clientId].inMultiplayerSession)
- {
- this.emitEvent("clientLeft", payload);
- userListChanged = true;
- }
- if (payload.inMultiplayerSession && !this._clients[payload.clientId].inMultiplayerSession)
- {
- this.emitEvent("clientJoined", payload);
- userListChanged = true;
- }
- }
- else
- {
- userListChanged = true;
- }
- this._clients[payload.clientId] = client;
- }
-
- if (this._connection.inMultiplayerSession)
- {
- let newPilot = null;
- if (payload.isPilot && !payload.isRemoteClient)
- {
- const keys = Object.keys(this._clients);
- for (let i = 0; i < keys.length; i++)
- {
- const client = this._clients[keys[i]];
- if (client.clientId !== payload.clientId)
- {
- client.isPilot = false;
- }
- else
- {
- if (client.clientId === this._connection.clientId && gui.isRemoteClient) continue;
- client.isPilot = true;
- newPilot = client;
- }
- }
- if (newPilot && (!this._pilot || newPilot.clientId !== this._pilot.clientId))
- {
- if (!newPilot.isRemoteClient)
- {
- userListChanged = true;
- this._pilot = newPilot;
- this.emitEvent("pilotChanged", newPilot);
- }
- }
- }
- else if (this._pilot)
- {
- if (this._pilot.clientId === payload.clientId && !payload.isPilot)
- {
- // pilot left the multiplayer session but is still in socketcluster
- this._pilot = null;
- this.emitEvent("pilotRemoved");
- }
- }
-
- if (payload.following && (payload.following === this._connection.clientId) && !this._followers.includes(payload.clientId))
- {
- this._followers.push(payload.clientId);
- userListChanged = true;
- }
- else if (!payload.following && this._followers.includes(payload.clientId))
- {
- this._followers = this._followers.filter((followerId) => { return followerId !== payload.clientId; });
- userListChanged = true;
- }
- }
- else if (payload.startedSession)
- {
- userListChanged = true;
- }
-
- const cleanupChange = this._cleanUpUserList();
- if (userListChanged || cleanupChange)
- {
- this.emitEvent("userListChanged");
- }
- }
-
-
-
- getNumClients()
- {
- return Object.keys(this._clients).length;
- }
-
- _cleanUpUserList()
- {
- // wait for patch to be in a synced state to update userlist
- if (!this._connection.synced)
- {
- return false;
- }
-
- const timeOutSeconds = this._connection.PING_INTERVAL * this._connection.PINGS_TO_TIMEOUT;
-
- let cleanupChange = false;
-
- Object.keys(this._clients).forEach((clientId) =>
- {
- const client = this._clients[clientId];
-
- if (client.lastSeen && (this._connection.getTimestamp() - client.lastSeen) > timeOutSeconds)
- {
- if (this._connection.clientId !== clientId)
- {
- this.emitEvent("clientRemoved", this._clients[client.clientId]);
- delete this._clients[client.clientId];
- }
- if (this._pilot && this._pilot.clientId === client.clientId)
- {
- this._pilot = null;
- this.emitEvent("pilotRemoved");
- }
- if (this.followers.includes(client.clientId)) this._followers = this._followers.filter((followerId) => { return followerId != client.clientId; });
- cleanupChange = true;
- }
- });
-
- if (this.getNumClients() < 2 && this._clients[this._connection.clientId] && !this._clients[this._connection.clientId].isPilot)
- {
- if (this._connection.inMultiplayerSession && !gui.isRemoteClient)
- {
- this._clients[this._connection.clientId].isPilot = true;
- cleanupChange = true;
- }
- }
-
- if (!this.hasPilot() && this._connection.inMultiplayerSession)
- {
- let pilot = null;
- let earliestConnection = this._connection.getTimestamp();
- Object.keys(this._clients).forEach((key) =>
- {
- const client = this._clients[key];
- if (client && client.isPilot) pilot = client;
- });
-
- if (!pilot)
- {
- // connection has no pilot, try to find the longest connected client that is also in a multiplayer session
- Object.keys(this._clients).forEach((key) =>
- {
- const client = this._clients[key];
- if (!client.isRemoteClient && client.inMultiplayerSession && client.inSessionSince && client.inSessionSince < earliestConnection)
- {
- pilot = client;
- earliestConnection = client.inSessionSince;
- }
- });
- }
-
- if (pilot && !pilot.isRemoteClient)
- {
- this._clients[pilot.clientId].isPilot = true;
- if (pilot.clientId === this._connection.clientId)
- {
- this.becomePilot();
- }
- }
- }
-
- return cleanupChange;
- }
-
- getPilot()
- {
- return this._pilot;
- }
-
- hasPilot()
- {
- return !!this._pilot;
- }
-
- becomePilot()
- {
- if (!gui.isRemoteClient)
- {
- this._log.verbose("this client became multiplayer pilot");
- this._connection.client.isPilot = true;
- this.emitEvent("becamePilot");
- gui.setRestriction(Gui.RESTRICT_MODE_FULL);
- }
- }
-
- requestPilotSeat()
- {
- const client = this._clients[this._connection.clientId];
- if (!gui.isRemoteClient && (client && !client.isPilot))
- {
- this._connection.sendControl("pilotRequest", { "username": client.username, "state": "request" });
- const myAvatar = document.querySelector("#multiplayerbar .sc-userlist .item.me");
- if (myAvatar) myAvatar.classList.add("pilot-request");
- this._pendingPilotRequest = setTimeout(() =>
- {
- if (this._pendingPilotRequest)
- {
- this.acceptPilotSeatRequest();
- this._pendingPilotRequest = null;
- }
- }, this.PILOT_REQUEST_TIMEOUT + 2000);
- }
- }
-
- hasPendingPilotSeatRequest()
- {
- return !!this._pendingPilotRequest;
- }
-
- acceptPilotSeatRequest()
- {
- const client = this._clients[this._connection.clientId];
- if (client && !client.isPilot && this._pendingPilotRequest)
- {
- clearTimeout(this._pendingPilotRequest);
- const myAvatar = document.querySelector("#multiplayerbar .sc-userlist .item.me");
- if (myAvatar) myAvatar.classList.add("pilot-request");
- this.becomePilot();
- }
- }
-
- cancelPilotSeatRequest()
- {
- const client = this._clients[this._connection.clientId];
- if (client && this._pendingPilotRequest)
- {
- clearTimeout(this._pendingPilotRequest);
- const myAvatar = document.querySelector("#multiplayerbar .sc-userlist .item.me");
- if (myAvatar) myAvatar.classList.remove("pilot-request");
- }
- }
-
- _registerEventListeners()
- {
- this._connection.on("onPingAnswer", this._onPingAnswer.bind(this));
- this._connection.on("netCursorPos", (msg) =>
- {
- if (this._connection.client.isRemoteClient) return;
- if (this._clients[msg.clientId])
- {
- if (this._clients[msg.clientId].subpatch != msg.subpatch)
- {
- this._clients[msg.clientId].subpatch = msg.subpatch;
- gui.emitEvent("multiUserSubpatchChanged", msg.clientId, msg.subpatch);
- }
-
- this._clients[msg.clientId].x = msg.x;
- this._clients[msg.clientId].y = msg.y;
- this._clients[msg.clientId].subpatch = msg.subpatch;
- this._clients[msg.clientId].zoom = msg.zoom;
- this._clients[msg.clientId].center = msg.center;
- this._clients[msg.clientId].scrollX = msg.scrollX;
- this._clients[msg.clientId].scrollY = msg.scrollY;
- }
- });
-
- this.on("clientDisconnected", (client, wasInMultiplayerSession = false) =>
- {
- gui.emitEvent("netClientRemoved", { "clientId": client.clientId });
- });
-
- this.on("clientLeft", (client) =>
- {
- gui.emitEvent("netClientRemoved", { "clientId": client.clientId });
- });
-
- this.on("patchSynchronized", () =>
- {
- // if (!this._connection.client.isPilot)
- // {
- // // set patchsave state if not pilot after sync
- // // gui.setStateSaved();
- // gui.savedState.setSaved("sc", 0);
- // }
- if (this._connection.client.isRemoteClient)
- {
- const menubar = document.getElementById("menubar");
- if (menubar) menubar.classList.add("hidden");
- }
- });
-
- this._connection.on("clientRemoved", (msg) =>
- {
- this._connection.sendUi("netClientRemoved", msg, true);
- gui.emitEvent("netClientRemoved", msg);
- });
-
- gui.patchView.on("mouseMove", (x, y) =>
- {
- // if (!this._connection.inMultiplayerSession) return;
- this._sendCursorPos(x, y);
- });
-
- gui.on("netOpPos", (payload) =>
- {
- if (!this._connection.inMultiplayerSession) return;
- if (this._connection.client && this._connection.client.isPilot)
- {
- this._connection.sendUi("netOpPos", payload);
- }
- });
-
- gui.on("timelineControl", (command, value) =>
- {
- if (!this._connection.inMultiplayerSession) return;
- if (this._connection.client && this._connection.client.isPilot)
- {
- if (command !== "scrollTime")
- {
- const payload = {
- "command": command,
- "value": value
- };
- this._connection.sendUi("timelineControl", payload);
- }
- else
- {
- if (this._timelineTimeout) return;
-
- const payload = {
- "command": "setTime",
- "value": value
- };
- this._timelineTimeout = setTimeout(() =>
- {
- this._connection.sendUi("timelineControl", payload);
- this._timelineTimeout = null;
- }, this._connection.netTimelineScrollDelay);
- }
- }
- });
-
- // gui.opParams.addEventListener("opSelected", (op) =>
- // {
- // if (!this._connection.inMultiplayerSession) return;
- // if (this._connection.client && this._connection.client.isPilot)
- // {
- // if (op)
- // this._connection.sendUi("opSelected", { "opId": op.id });
- // }
- // });
-
- // this._connection.on("opSelected", (msg) =>
- // {
- // if (!this._connection.inMultiplayerSession) return;
- // if (this._connection.client.isRemoteClient) return;
- // if (!this._connection.client.following) return;
- // if (!this._connection.client.following === msg.clientId) return;
- // const op = gui.corePatch().getOpById(msg.opId);
- // if (op)
- // {
- // gui.patchView.unselectAllOps();
- // gui.patchView.selectOpId(msg.opId);
- // gui.patchView.focusOp(msg.opId);
- // }
- // });
-
- this._connection.on("timelineControl", (msg) =>
- {
- if (!this._connection.inMultiplayerSession) return;
- const timeline = gui.timeLine();
- if (!timeline) return;
-
- switch (msg.command)
- {
- case "setTime":
- if (msg.hasOwnProperty("value"))
- {
- gui.timeLine().gotoTime(msg.value);
- }
- break;
- case "setPlay":
- const timer = gui.scene().timer;
- if (timer)
- {
- const targetState = !!msg.value;
- const isPlaying = timer.isPlaying();
- if (targetState !== isPlaying)
- {
- timeline.togglePlay();
- }
- if (msg.hasOwnProperty("time"))
- {
- gui.timeLine().gotoTime(msg.time);
- }
- }
- break;
- case "setLoop":
- timeline.setLoop(msg.value);
- break;
- case "setAnim":
- timeline.setAnim(msg.value.newanim, msg.value.config);
- break;
- case "setLength":
- timeline.setTimeLineLength(msg.value);
- break;
- }
- });
-
- gui.on("portValueEdited", (op, port, value) =>
- {
- if (!this._connection.inMultiplayerSession) return;
- if (this._connection.client) // && this._connection.client.isPilot)
- {
- if (op && port)
- {
- const payload = {};
- payload.data = {
- "event": CABLES.PACO_VALUECHANGE,
- "vars": {
- "op": op.id,
- "port": port.name,
- "v": value
- }
- };
- this._connection.sendPaco(payload);
- }
- }
- });
-
- gui.corePatch().on("pacoPortValueSetAnimated", (op, index, targetState, defaultValue) =>
- {
- if (!this._connection.inMultiplayerSession) return;
- CABLES.UI.paramsHelper.setPortAnimated(op, index, targetState, defaultValue);
- });
-
- gui.corePatch().on("pacoPortAnimUpdated", (port) =>
- {
- if (!port.anim) return;
- if (!this._connection.inMultiplayerSession) return;
- gui.metaKeyframes.showAnim(port.parent.id, port.name);
- });
-
- gui.on("portValueSetAnimated", (op, portIndex, targetState, defaultValue) =>
- {
- if (!this._connection.inMultiplayerSession) return;
- if (this._connection.client && this._connection.client.isPilot)
- {
- if (op)
- {
- const payload = {};
- payload.data = {
- "event": CABLES.PACO_PORT_SETANIMATED,
- "vars": {
- "opId": op.id,
- "portIndex": portIndex,
- "targetState": targetState,
- "defaultValue": defaultValue
- }
- };
- this._connection.sendPaco(payload);
- }
- }
- });
-
- gui.corePatch().on("opReloaded", (opName) =>
- {
- if (!this._connection.inMultiplayerSession) return;
- if (this._connection.client && this._connection.client.isPilot)
- {
- this._connection.sendControl("reloadOp", { "opName": opName });
- }
- });
-
- gui.on("drawSelectionArea", (x, y, sizeX, sizeY) =>
- {
- // if (!this._connection.inMultiplayerSession) return;
- this._sendSelectionArea(x, y, sizeX, sizeY);
- });
-
- gui.on("hideSelectionArea", (x, y, sizeX, sizeY) =>
- {
- // if (!this._connection.inMultiplayerSession) return;
- this._sendSelectionArea(x, y, sizeX, sizeY, true);
- });
-
- gui.on("gizmoMove", (opId, portName, newValue) =>
- {
- if (!this._connection.inMultiplayerSession) return;
- if (this._connection.client && this._connection.client.isPilot)
- {
- if (opId && portName)
- {
- const payload = {};
- payload.data = {
- "event": CABLES.PACO_VALUECHANGE,
- "vars": {
- "op": opId,
- "port": portName,
- "v": newValue
- }
- };
- this._connection.sendPaco(payload);
- }
- }
- });
-
- this._connection.on("netOpPos", (msg) =>
- {
- if (!this._connection.inMultiplayerSession) return;
- if (this._connection.client.isRemoteClient) return;
- const op = gui.corePatch().getOpById(msg.opId);
- if (op)
- {
- op.setUiAttrib({ "translate": { "x": msg.x, "y": msg.y } });
- }
- else
- {
- setTimeout(
- () =>
- {
- this._connection.emitEvent("netOpPos", msg);
- }, 100);
- }
- });
-
- this._connection.on("netSelectionArea", (msg) =>
- {
- gui.emitEvent("netSelectionArea", msg);
- });
-
- this._connection.on("netCursorPos", (msg) =>
- {
- // if (!this._connection.inMultiplayerSession) return;
- delete msg.zoom;
- // if (this._connection.client.following && msg.clientId === this._connection.client.following)
- // {
- // gui.emitEvent("netGotoPos", msg);
- // }
- gui.emitEvent("netCursorPos", msg);
- });
-
- this._connection.on("resyncWithPilot", (msg) =>
- {
- if (!this._connection.inMultiplayerSession) return;
- if (!this._connection.client.isRemoteClient) return;
- if (this._connection.clientId !== msg.reloadClient) return;
- this._connection.requestPilotPatch();
- });
-
- this._connection.on("onPortValueChanged", (vars) =>
- {
- if (!this._connection.inMultiplayerSession) return;
- if (this._connection.client.isRemoteClient) return;
- if (this._connection.client.isPilot) return;
-
- const selectedOp = gui.patchView.getSelectedOps().find((op) => { return op.id === vars.op; });
- if (selectedOp)
- {
- const portIndex = selectedOp.portsIn.findIndex((port) => { return port.name === vars.port; });
- if (portIndex)
- {
- clearTimeout(this._timeoutRefresh);
- this._timeoutRefresh = setTimeout(() =>
- {
- selectedOp.refreshParams();
- }, 50);
-
-
- // const elePortId = "portval_" + portIndex;
- // const elePort = document.getElementById(elePortId);
- // if (elePort)
- // {
- // gui.opParams.refreshDelayed();
- // const elePortContainer = document.getElementById("tr_in_" + portIndex);
- // if (elePortContainer)
- // {
- // elePortContainer.scrollIntoView({ "block": "center" });
- // }
- // }
- }
- }
- });
- }
-
- _sendCursorPos(x, y)
- {
- if (!this._connection.isConnected()) return;
- // if (!this._connection.inMultiplayerSession) return;
-
- if (this._lastMouseX === x || this._lastMouseY === y) return;
-
-
- this._lastMouseX = x;
- this._lastMouseY = y;
-
- if (this._mouseTimeout) return;
-
- const subPatch = gui.patchView.getCurrentSubPatch();
- const zoom = gui.patchView.patchRenderer.viewBox ? gui.patchView.patchRenderer.viewBox.zoom : null;
- const scrollX = gui.patchView.patchRenderer.viewBox ? gui.patchView.patchRenderer.viewBox.scrollX : null;
- const scrollY = gui.patchView.patchRenderer.viewBox ? gui.patchView.patchRenderer.viewBox.scrollY : null;
-
-
- this._mouseTimeout = setTimeout(() =>
- {
- const payload = { "x": this._lastMouseX, "y": this._lastMouseY, "subpatch": subPatch, "zoom": zoom, "scrollX": scrollX, "scrollY": scrollY };
- this._connection.sendUi("netCursorPos", payload);
- this._mouseTimeout = null;
- }, this._connection.netMouseCursorDelay);
- }
-
- _sendSelectionArea(x, y, sizeX, sizeY, hide = false)
- {
- return;
- if (!this._connection.isConnected()) return;
- if (!this._connection.inMultiplayerSession) return;
-
- if (!hide && this._mouseTimeout) return;
-
- this._mouseTimeout = setTimeout(() =>
- {
- const payload = { x, y, sizeX, sizeY, hide };
- this._connection.sendUi("netSelectionArea", payload);
- this._mouseTimeout = null;
- }, this._connection.netMouseCursorDelay);
- }
- }