private void RegisterVirtualPlayerRPC(string virtualId, int photonPlayerId)
    {
        if (!PhotonNetwork.isMasterClient)
        {
            return;
        }
        if (virtualPlayers.ContainsKey(virtualId))
        {
            // Already registered.
            return;
        }
        // Allocate the lowest free slot#
        int    slotNumber = GetLowestFreeSlotNumber();
        string nickName   = "Player" + slotNumber;

        foreach (PhotonPlayer player in PhotonNetwork.playerList)
        {
            if (player.ID == photonPlayerId && !string.IsNullOrEmpty(player.NickName))
            {
                nickName = player.NickName;
            }
        }
        VirtualPlayerInfo info = new VirtualPlayerInfo
        {
            virtualId      = virtualId,
            photonPlayerId = photonPlayerId,
            slotNumber     = slotNumber,
            nickName       = nickName
        };

        PutVirtualPlayerLocal(info);
        photonView.RPC("PutVirtualPlayerRPC", PhotonTargets.AllViaServer, JsonUtility.ToJson(info));
    }
    private void PutVirtualPlayerLocal(VirtualPlayerInfo info)
    {
        bool isNew = !virtualPlayers.ContainsKey(info.virtualId);

        virtualPlayers[info.virtualId] = info;
        if (isNew)
        {
            onVirtualPlayerJoined?.Invoke(info.virtualId);
        }
    }
    public void SetNickName(string virtualId, string nickName)
    {
        VirtualPlayerInfo?maybeInfo = GetVirtualPlayerById(virtualId);

        if (maybeInfo != null)
        {
            VirtualPlayerInfo info = maybeInfo.Value;
            info.nickName = nickName;
            // This doesn't happen often (it's a debug command), so let's piggy back on an existing RPC:
            photonView.RPC("PutVirtualPlayerRPC", PhotonTargets.AllViaServer, JsonUtility.ToJson(info));
        }
    }