Home Reference Source

cables_dev/cables_ui/src/ui/multiplayer/sc_state.js

  1. import { Logger, Events } from "cables-shared-client";
  2. import Gui from "../gui.js";
  3. import ScClient from "./sc_client.js";
  4.  
  5. CABLES = CABLES || {};
  6.  
  7. export default class ScState extends Events
  8. {
  9. constructor(connection)
  10. {
  11. super();
  12.  
  13. this.PILOT_REQUEST_TIMEOUT = 20000;
  14.  
  15. this._log = new Logger("scstate");
  16.  
  17. this._connection = connection;
  18.  
  19. this._clients = {};
  20. this._clients[connection.clientId] = new ScClient({
  21. "username": gui.user.username,
  22. "userid": gui.user.id,
  23. "clientId": connection.clientId,
  24. "isMe": true,
  25. "isRemoteClient": gui.isRemoteClient,
  26. "multiplayerCapable": this._connection.multiplayerCapable,
  27. "isPilot": false
  28. });
  29. this._followers = [];
  30. this._colors = {};
  31. this._pilot = null;
  32. this._timeoutRefresh = null;
  33.  
  34. this._registerEventListeners();
  35. }
  36.  
  37. get clients() { return this._clients; }
  38.  
  39. get followers() { return this._followers; }
  40.  
  41.  
  42. getUserId(clientId)
  43. {
  44. if (this._clients[clientId])
  45. return this._clients[clientId].userid;
  46. }
  47.  
  48.  
  49. getUserInSubpatch(subPatch)
  50. {
  51. const userIds = [];
  52. for (const i in this._clients)
  53. {
  54. if (!this._clients[i].isMe && this._clients[i].subpatch == subPatch)
  55. userIds.push(this._clients[i].userid);
  56. }
  57.  
  58. return userIds;
  59. }
  60.  
  61. _onPingAnswer(payload)
  62. {
  63. let userListChanged = false;
  64. if (payload.isDisconnected)
  65. {
  66. if (this._clients[payload.clientId])
  67. {
  68. const wasInMultiplayerSession = this._clients[payload.clientId].inMultiplayerSession;
  69. if (this._connection.clientId !== payload.clientId)
  70. {
  71. delete this._clients[payload.clientId];
  72. this.emitEvent("clientDisconnected", payload, wasInMultiplayerSession);
  73. userListChanged = true;
  74. }
  75. }
  76. }
  77. else
  78. {
  79. const client = new ScClient(payload, this._connection.client);
  80. if (this._clients[payload.clientId])
  81. {
  82. if (!payload.inMultiplayerSession && this._clients[payload.clientId].inMultiplayerSession)
  83. {
  84. this.emitEvent("clientLeft", payload);
  85. userListChanged = true;
  86. }
  87. if (payload.inMultiplayerSession && !this._clients[payload.clientId].inMultiplayerSession)
  88. {
  89. this.emitEvent("clientJoined", payload);
  90. userListChanged = true;
  91. }
  92. }
  93. else
  94. {
  95. userListChanged = true;
  96. }
  97. this._clients[payload.clientId] = client;
  98. }
  99.  
  100. if (this._connection.inMultiplayerSession)
  101. {
  102. let newPilot = null;
  103. if (payload.isPilot && !payload.isRemoteClient)
  104. {
  105. const keys = Object.keys(this._clients);
  106. for (let i = 0; i < keys.length; i++)
  107. {
  108. const client = this._clients[keys[i]];
  109. if (client.clientId !== payload.clientId)
  110. {
  111. client.isPilot = false;
  112. }
  113. else
  114. {
  115. if (client.clientId === this._connection.clientId && gui.isRemoteClient) continue;
  116. client.isPilot = true;
  117. newPilot = client;
  118. }
  119. }
  120. if (newPilot && (!this._pilot || newPilot.clientId !== this._pilot.clientId))
  121. {
  122. if (!newPilot.isRemoteClient)
  123. {
  124. userListChanged = true;
  125. this._pilot = newPilot;
  126. this.emitEvent("pilotChanged", newPilot);
  127. }
  128. }
  129. }
  130. else if (this._pilot)
  131. {
  132. if (this._pilot.clientId === payload.clientId && !payload.isPilot)
  133. {
  134. // pilot left the multiplayer session but is still in socketcluster
  135. this._pilot = null;
  136. this.emitEvent("pilotRemoved");
  137. }
  138. }
  139.  
  140. if (payload.following && (payload.following === this._connection.clientId) && !this._followers.includes(payload.clientId))
  141. {
  142. this._followers.push(payload.clientId);
  143. userListChanged = true;
  144. }
  145. else if (!payload.following && this._followers.includes(payload.clientId))
  146. {
  147. this._followers = this._followers.filter((followerId) => { return followerId !== payload.clientId; });
  148. userListChanged = true;
  149. }
  150. }
  151. else if (payload.startedSession)
  152. {
  153. userListChanged = true;
  154. }
  155.  
  156. const cleanupChange = this._cleanUpUserList();
  157. if (userListChanged || cleanupChange)
  158. {
  159. this.emitEvent("userListChanged");
  160. }
  161. }
  162.  
  163.  
  164.  
  165. getNumClients()
  166. {
  167. return Object.keys(this._clients).length;
  168. }
  169.  
  170. _cleanUpUserList()
  171. {
  172. // wait for patch to be in a synced state to update userlist
  173. if (!this._connection.synced)
  174. {
  175. return false;
  176. }
  177.  
  178. const timeOutSeconds = this._connection.PING_INTERVAL * this._connection.PINGS_TO_TIMEOUT;
  179.  
  180. let cleanupChange = false;
  181.  
  182. Object.keys(this._clients).forEach((clientId) =>
  183. {
  184. const client = this._clients[clientId];
  185.  
  186. if (client.lastSeen && (this._connection.getTimestamp() - client.lastSeen) > timeOutSeconds)
  187. {
  188. if (this._connection.clientId !== clientId)
  189. {
  190. this.emitEvent("clientRemoved", this._clients[client.clientId]);
  191. delete this._clients[client.clientId];
  192. }
  193. if (this._pilot && this._pilot.clientId === client.clientId)
  194. {
  195. this._pilot = null;
  196. this.emitEvent("pilotRemoved");
  197. }
  198. if (this.followers.includes(client.clientId)) this._followers = this._followers.filter((followerId) => { return followerId != client.clientId; });
  199. cleanupChange = true;
  200. }
  201. });
  202.  
  203. if (this.getNumClients() < 2 && this._clients[this._connection.clientId] && !this._clients[this._connection.clientId].isPilot)
  204. {
  205. if (this._connection.inMultiplayerSession && !gui.isRemoteClient)
  206. {
  207. this._clients[this._connection.clientId].isPilot = true;
  208. cleanupChange = true;
  209. }
  210. }
  211.  
  212. if (!this.hasPilot() && this._connection.inMultiplayerSession)
  213. {
  214. let pilot = null;
  215. let earliestConnection = this._connection.getTimestamp();
  216. Object.keys(this._clients).forEach((key) =>
  217. {
  218. const client = this._clients[key];
  219. if (client && client.isPilot) pilot = client;
  220. });
  221.  
  222. if (!pilot)
  223. {
  224. // connection has no pilot, try to find the longest connected client that is also in a multiplayer session
  225. Object.keys(this._clients).forEach((key) =>
  226. {
  227. const client = this._clients[key];
  228. if (!client.isRemoteClient && client.inMultiplayerSession && client.inSessionSince && client.inSessionSince < earliestConnection)
  229. {
  230. pilot = client;
  231. earliestConnection = client.inSessionSince;
  232. }
  233. });
  234. }
  235.  
  236. if (pilot && !pilot.isRemoteClient)
  237. {
  238. this._clients[pilot.clientId].isPilot = true;
  239. if (pilot.clientId === this._connection.clientId)
  240. {
  241. this.becomePilot();
  242. }
  243. }
  244. }
  245.  
  246. return cleanupChange;
  247. }
  248.  
  249. getPilot()
  250. {
  251. return this._pilot;
  252. }
  253.  
  254. hasPilot()
  255. {
  256. return !!this._pilot;
  257. }
  258.  
  259. becomePilot()
  260. {
  261. if (!gui.isRemoteClient)
  262. {
  263. this._log.verbose("this client became multiplayer pilot");
  264. this._connection.client.isPilot = true;
  265. this.emitEvent("becamePilot");
  266. gui.setRestriction(Gui.RESTRICT_MODE_FULL);
  267. }
  268. }
  269.  
  270. requestPilotSeat()
  271. {
  272. const client = this._clients[this._connection.clientId];
  273. if (!gui.isRemoteClient && (client && !client.isPilot))
  274. {
  275. this._connection.sendControl("pilotRequest", { "username": client.username, "state": "request" });
  276. const myAvatar = document.querySelector("#multiplayerbar .sc-userlist .item.me");
  277. if (myAvatar) myAvatar.classList.add("pilot-request");
  278. this._pendingPilotRequest = setTimeout(() =>
  279. {
  280. if (this._pendingPilotRequest)
  281. {
  282. this.acceptPilotSeatRequest();
  283. this._pendingPilotRequest = null;
  284. }
  285. }, this.PILOT_REQUEST_TIMEOUT + 2000);
  286. }
  287. }
  288.  
  289. hasPendingPilotSeatRequest()
  290. {
  291. return !!this._pendingPilotRequest;
  292. }
  293.  
  294. acceptPilotSeatRequest()
  295. {
  296. const client = this._clients[this._connection.clientId];
  297. if (client && !client.isPilot && this._pendingPilotRequest)
  298. {
  299. clearTimeout(this._pendingPilotRequest);
  300. const myAvatar = document.querySelector("#multiplayerbar .sc-userlist .item.me");
  301. if (myAvatar) myAvatar.classList.add("pilot-request");
  302. this.becomePilot();
  303. }
  304. }
  305.  
  306. cancelPilotSeatRequest()
  307. {
  308. const client = this._clients[this._connection.clientId];
  309. if (client && this._pendingPilotRequest)
  310. {
  311. clearTimeout(this._pendingPilotRequest);
  312. const myAvatar = document.querySelector("#multiplayerbar .sc-userlist .item.me");
  313. if (myAvatar) myAvatar.classList.remove("pilot-request");
  314. }
  315. }
  316.  
  317. _registerEventListeners()
  318. {
  319. this._connection.on("onPingAnswer", this._onPingAnswer.bind(this));
  320. this._connection.on("netCursorPos", (msg) =>
  321. {
  322. if (this._connection.client.isRemoteClient) return;
  323. if (this._clients[msg.clientId])
  324. {
  325. if (this._clients[msg.clientId].subpatch != msg.subpatch)
  326. {
  327. this._clients[msg.clientId].subpatch = msg.subpatch;
  328. gui.emitEvent("multiUserSubpatchChanged", msg.clientId, msg.subpatch);
  329. }
  330.  
  331. this._clients[msg.clientId].x = msg.x;
  332. this._clients[msg.clientId].y = msg.y;
  333. this._clients[msg.clientId].subpatch = msg.subpatch;
  334. this._clients[msg.clientId].zoom = msg.zoom;
  335. this._clients[msg.clientId].center = msg.center;
  336. this._clients[msg.clientId].scrollX = msg.scrollX;
  337. this._clients[msg.clientId].scrollY = msg.scrollY;
  338. }
  339. });
  340.  
  341. this.on("clientDisconnected", (client, wasInMultiplayerSession = false) =>
  342. {
  343. gui.emitEvent("netClientRemoved", { "clientId": client.clientId });
  344. });
  345.  
  346. this.on("clientLeft", (client) =>
  347. {
  348. gui.emitEvent("netClientRemoved", { "clientId": client.clientId });
  349. });
  350.  
  351. this.on("patchSynchronized", () =>
  352. {
  353. // if (!this._connection.client.isPilot)
  354. // {
  355. // // set patchsave state if not pilot after sync
  356. // // gui.setStateSaved();
  357. // gui.savedState.setSaved("sc", 0);
  358. // }
  359. if (this._connection.client.isRemoteClient)
  360. {
  361. const menubar = document.getElementById("menubar");
  362. if (menubar) menubar.classList.add("hidden");
  363. }
  364. });
  365.  
  366. this._connection.on("clientRemoved", (msg) =>
  367. {
  368. this._connection.sendUi("netClientRemoved", msg, true);
  369. gui.emitEvent("netClientRemoved", msg);
  370. });
  371.  
  372. gui.patchView.on("mouseMove", (x, y) =>
  373. {
  374. // if (!this._connection.inMultiplayerSession) return;
  375. this._sendCursorPos(x, y);
  376. });
  377.  
  378. gui.on("netOpPos", (payload) =>
  379. {
  380. if (!this._connection.inMultiplayerSession) return;
  381. if (this._connection.client && this._connection.client.isPilot)
  382. {
  383. this._connection.sendUi("netOpPos", payload);
  384. }
  385. });
  386.  
  387. gui.on("timelineControl", (command, value) =>
  388. {
  389. if (!this._connection.inMultiplayerSession) return;
  390. if (this._connection.client && this._connection.client.isPilot)
  391. {
  392. if (command !== "scrollTime")
  393. {
  394. const payload = {
  395. "command": command,
  396. "value": value
  397. };
  398. this._connection.sendUi("timelineControl", payload);
  399. }
  400. else
  401. {
  402. if (this._timelineTimeout) return;
  403.  
  404. const payload = {
  405. "command": "setTime",
  406. "value": value
  407. };
  408. this._timelineTimeout = setTimeout(() =>
  409. {
  410. this._connection.sendUi("timelineControl", payload);
  411. this._timelineTimeout = null;
  412. }, this._connection.netTimelineScrollDelay);
  413. }
  414. }
  415. });
  416.  
  417. // gui.opParams.addEventListener("opSelected", (op) =>
  418. // {
  419. // if (!this._connection.inMultiplayerSession) return;
  420. // if (this._connection.client && this._connection.client.isPilot)
  421. // {
  422. // if (op)
  423. // this._connection.sendUi("opSelected", { "opId": op.id });
  424. // }
  425. // });
  426.  
  427. // this._connection.on("opSelected", (msg) =>
  428. // {
  429. // if (!this._connection.inMultiplayerSession) return;
  430. // if (this._connection.client.isRemoteClient) return;
  431. // if (!this._connection.client.following) return;
  432. // if (!this._connection.client.following === msg.clientId) return;
  433. // const op = gui.corePatch().getOpById(msg.opId);
  434. // if (op)
  435. // {
  436. // gui.patchView.unselectAllOps();
  437. // gui.patchView.selectOpId(msg.opId);
  438. // gui.patchView.focusOp(msg.opId);
  439. // }
  440. // });
  441.  
  442. this._connection.on("timelineControl", (msg) =>
  443. {
  444. if (!this._connection.inMultiplayerSession) return;
  445. const timeline = gui.timeLine();
  446. if (!timeline) return;
  447.  
  448. switch (msg.command)
  449. {
  450. case "setTime":
  451. if (msg.hasOwnProperty("value"))
  452. {
  453. gui.timeLine().gotoTime(msg.value);
  454. }
  455. break;
  456. case "setPlay":
  457. const timer = gui.scene().timer;
  458. if (timer)
  459. {
  460. const targetState = !!msg.value;
  461. const isPlaying = timer.isPlaying();
  462. if (targetState !== isPlaying)
  463. {
  464. timeline.togglePlay();
  465. }
  466. if (msg.hasOwnProperty("time"))
  467. {
  468. gui.timeLine().gotoTime(msg.time);
  469. }
  470. }
  471. break;
  472. case "setLoop":
  473. timeline.setLoop(msg.value);
  474. break;
  475. case "setAnim":
  476. timeline.setAnim(msg.value.newanim, msg.value.config);
  477. break;
  478. case "setLength":
  479. timeline.setTimeLineLength(msg.value);
  480. break;
  481. }
  482. });
  483.  
  484. gui.on("portValueEdited", (op, port, value) =>
  485. {
  486. if (!this._connection.inMultiplayerSession) return;
  487. if (this._connection.client) // && this._connection.client.isPilot)
  488. {
  489. if (op && port)
  490. {
  491. const payload = {};
  492. payload.data = {
  493. "event": CABLES.PACO_VALUECHANGE,
  494. "vars": {
  495. "op": op.id,
  496. "port": port.name,
  497. "v": value
  498. }
  499. };
  500. this._connection.sendPaco(payload);
  501. }
  502. }
  503. });
  504.  
  505. gui.corePatch().on("pacoPortValueSetAnimated", (op, index, targetState, defaultValue) =>
  506. {
  507. if (!this._connection.inMultiplayerSession) return;
  508. CABLES.UI.paramsHelper.setPortAnimated(op, index, targetState, defaultValue);
  509. });
  510.  
  511. gui.corePatch().on("pacoPortAnimUpdated", (port) =>
  512. {
  513. if (!port.anim) return;
  514. if (!this._connection.inMultiplayerSession) return;
  515. gui.metaKeyframes.showAnim(port.parent.id, port.name);
  516. });
  517.  
  518. gui.on("portValueSetAnimated", (op, portIndex, targetState, defaultValue) =>
  519. {
  520. if (!this._connection.inMultiplayerSession) return;
  521. if (this._connection.client && this._connection.client.isPilot)
  522. {
  523. if (op)
  524. {
  525. const payload = {};
  526. payload.data = {
  527. "event": CABLES.PACO_PORT_SETANIMATED,
  528. "vars": {
  529. "opId": op.id,
  530. "portIndex": portIndex,
  531. "targetState": targetState,
  532. "defaultValue": defaultValue
  533. }
  534. };
  535. this._connection.sendPaco(payload);
  536. }
  537. }
  538. });
  539.  
  540. gui.corePatch().on("opReloaded", (opName) =>
  541. {
  542. if (!this._connection.inMultiplayerSession) return;
  543. if (this._connection.client && this._connection.client.isPilot)
  544. {
  545. this._connection.sendControl("reloadOp", { "opName": opName });
  546. }
  547. });
  548.  
  549. gui.on("drawSelectionArea", (x, y, sizeX, sizeY) =>
  550. {
  551. // if (!this._connection.inMultiplayerSession) return;
  552. this._sendSelectionArea(x, y, sizeX, sizeY);
  553. });
  554.  
  555. gui.on("hideSelectionArea", (x, y, sizeX, sizeY) =>
  556. {
  557. // if (!this._connection.inMultiplayerSession) return;
  558. this._sendSelectionArea(x, y, sizeX, sizeY, true);
  559. });
  560.  
  561. gui.on("gizmoMove", (opId, portName, newValue) =>
  562. {
  563. if (!this._connection.inMultiplayerSession) return;
  564. if (this._connection.client && this._connection.client.isPilot)
  565. {
  566. if (opId && portName)
  567. {
  568. const payload = {};
  569. payload.data = {
  570. "event": CABLES.PACO_VALUECHANGE,
  571. "vars": {
  572. "op": opId,
  573. "port": portName,
  574. "v": newValue
  575. }
  576. };
  577. this._connection.sendPaco(payload);
  578. }
  579. }
  580. });
  581.  
  582. this._connection.on("netOpPos", (msg) =>
  583. {
  584. if (!this._connection.inMultiplayerSession) return;
  585. if (this._connection.client.isRemoteClient) return;
  586. const op = gui.corePatch().getOpById(msg.opId);
  587. if (op)
  588. {
  589. op.setUiAttrib({ "translate": { "x": msg.x, "y": msg.y } });
  590. }
  591. else
  592. {
  593. setTimeout(
  594. () =>
  595. {
  596. this._connection.emitEvent("netOpPos", msg);
  597. }, 100);
  598. }
  599. });
  600.  
  601. this._connection.on("netSelectionArea", (msg) =>
  602. {
  603. gui.emitEvent("netSelectionArea", msg);
  604. });
  605.  
  606. this._connection.on("netCursorPos", (msg) =>
  607. {
  608. // if (!this._connection.inMultiplayerSession) return;
  609. delete msg.zoom;
  610. // if (this._connection.client.following && msg.clientId === this._connection.client.following)
  611. // {
  612. // gui.emitEvent("netGotoPos", msg);
  613. // }
  614. gui.emitEvent("netCursorPos", msg);
  615. });
  616.  
  617. this._connection.on("resyncWithPilot", (msg) =>
  618. {
  619. if (!this._connection.inMultiplayerSession) return;
  620. if (!this._connection.client.isRemoteClient) return;
  621. if (this._connection.clientId !== msg.reloadClient) return;
  622. this._connection.requestPilotPatch();
  623. });
  624.  
  625. this._connection.on("onPortValueChanged", (vars) =>
  626. {
  627. if (!this._connection.inMultiplayerSession) return;
  628. if (this._connection.client.isRemoteClient) return;
  629. if (this._connection.client.isPilot) return;
  630.  
  631. const selectedOp = gui.patchView.getSelectedOps().find((op) => { return op.id === vars.op; });
  632. if (selectedOp)
  633. {
  634. const portIndex = selectedOp.portsIn.findIndex((port) => { return port.name === vars.port; });
  635. if (portIndex)
  636. {
  637. clearTimeout(this._timeoutRefresh);
  638. this._timeoutRefresh = setTimeout(() =>
  639. {
  640. selectedOp.refreshParams();
  641. }, 50);
  642.  
  643.  
  644. // const elePortId = "portval_" + portIndex;
  645. // const elePort = document.getElementById(elePortId);
  646. // if (elePort)
  647. // {
  648. // gui.opParams.refreshDelayed();
  649. // const elePortContainer = document.getElementById("tr_in_" + portIndex);
  650. // if (elePortContainer)
  651. // {
  652. // elePortContainer.scrollIntoView({ "block": "center" });
  653. // }
  654. // }
  655. }
  656. }
  657. });
  658. }
  659.  
  660. _sendCursorPos(x, y)
  661. {
  662. if (!this._connection.isConnected()) return;
  663. // if (!this._connection.inMultiplayerSession) return;
  664.  
  665. if (this._lastMouseX === x || this._lastMouseY === y) return;
  666.  
  667.  
  668. this._lastMouseX = x;
  669. this._lastMouseY = y;
  670.  
  671. if (this._mouseTimeout) return;
  672.  
  673. const subPatch = gui.patchView.getCurrentSubPatch();
  674. const zoom = gui.patchView.patchRenderer.viewBox ? gui.patchView.patchRenderer.viewBox.zoom : null;
  675. const scrollX = gui.patchView.patchRenderer.viewBox ? gui.patchView.patchRenderer.viewBox.scrollX : null;
  676. const scrollY = gui.patchView.patchRenderer.viewBox ? gui.patchView.patchRenderer.viewBox.scrollY : null;
  677.  
  678.  
  679. this._mouseTimeout = setTimeout(() =>
  680. {
  681. const payload = { "x": this._lastMouseX, "y": this._lastMouseY, "subpatch": subPatch, "zoom": zoom, "scrollX": scrollX, "scrollY": scrollY };
  682. this._connection.sendUi("netCursorPos", payload);
  683. this._mouseTimeout = null;
  684. }, this._connection.netMouseCursorDelay);
  685. }
  686.  
  687. _sendSelectionArea(x, y, sizeX, sizeY, hide = false)
  688. {
  689. return;
  690. if (!this._connection.isConnected()) return;
  691. if (!this._connection.inMultiplayerSession) return;
  692.  
  693. if (!hide && this._mouseTimeout) return;
  694.  
  695. this._mouseTimeout = setTimeout(() =>
  696. {
  697. const payload = { x, y, sizeX, sizeY, hide };
  698. this._connection.sendUi("netSelectionArea", payload);
  699. this._mouseTimeout = null;
  700. }, this._connection.netMouseCursorDelay);
  701. }
  702. }