Home Reference Source

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);
    }
}