Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mumble-voip/mumble.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x.ci/azure-pipelines/install-environment_linux.bash3
-rw-r--r--.cirrus.yml2
-rwxr-xr-x.github/actions/install-dependencies/install_ubuntu_shared_64bit.sh3
-rw-r--r--CMakeLists.txt1
-rw-r--r--docs/dev/build-instructions/cmake_options.md10
-rw-r--r--plugins/CMakeLists.txt5
-rw-r--r--plugins/HostLinux.cpp2
-rw-r--r--plugins/HostWindows.cpp2
-rw-r--r--plugins/MumbleAPI_v_1_0_x.h492
-rw-r--r--plugins/MumblePlugin_v_1_0_x.h402
-rw-r--r--plugins/PluginComponents_v_1_0_x.h411
-rw-r--r--plugins/Process.cpp2
-rw-r--r--plugins/ProcessWindows.cpp2
-rw-r--r--plugins/amongus/Game.cpp2
-rw-r--r--plugins/amongus/amongus.cpp6
-rw-r--r--plugins/aoc/aoc.cpp5
-rw-r--r--plugins/arma2/arma2.cpp5
-rw-r--r--plugins/bf1/bf1.cpp7
-rw-r--r--plugins/bf1942/bf1942.cpp5
-rw-r--r--plugins/bf2/bf2.cpp5
-rw-r--r--plugins/bf2142/bf2142.cpp7
-rw-r--r--plugins/bf3/bf3.cpp5
-rw-r--r--plugins/bf4/bf4.cpp7
-rw-r--r--plugins/bf4_x86/bf4_x86.cpp7
-rw-r--r--plugins/bfbc2/bfbc2.cpp5
-rw-r--r--plugins/bfheroes/bfheroes.cpp5
-rw-r--r--plugins/blacklight/blacklight.cpp5
-rw-r--r--plugins/borderlands/borderlands.cpp5
-rw-r--r--plugins/borderlands2/borderlands2.cpp5
-rw-r--r--plugins/breach/breach.cpp5
-rw-r--r--plugins/cod2/cod2.cpp6
-rw-r--r--plugins/cod4/cod4.cpp5
-rw-r--r--plugins/cod5/cod5.cpp5
-rw-r--r--plugins/codmw2/codmw2.cpp5
-rw-r--r--plugins/codmw2so/codmw2so.cpp5
-rw-r--r--plugins/cs/cs.cpp5
-rw-r--r--plugins/dys/dys.cpp5
-rw-r--r--plugins/etqw/etqw.cpp5
-rw-r--r--plugins/ffxiv/ffxiv.cpp7
-rw-r--r--plugins/gmod/gmod.cpp5
-rw-r--r--plugins/gtaiv/gtaiv.cpp5
-rw-r--r--plugins/gtasa/gtasa.cpp5
-rw-r--r--plugins/gtav/gtav.cpp7
-rw-r--r--plugins/gw/gw.cpp5
-rw-r--r--plugins/insurgency/insurgency.cpp5
-rw-r--r--plugins/jc2/jc2.cpp5
-rw-r--r--plugins/link/link-posix.cpp3
-rw-r--r--plugins/link/link.cpp3
-rw-r--r--plugins/lol/lol.cpp5
-rw-r--r--plugins/lotro/lotro.cpp5
-rw-r--r--plugins/mumble_legacy_plugin.h (renamed from plugins/mumble_plugin.h)19
-rw-r--r--plugins/mumble_positional_audio_linux.h (renamed from plugins/mumble_plugin_linux.h)12
-rw-r--r--plugins/mumble_positional_audio_main.h (renamed from plugins/mumble_plugin_main.h)15
-rw-r--r--plugins/mumble_positional_audio_utils.h (renamed from plugins/mumble_plugin_utils.h)6
-rw-r--r--plugins/mumble_positional_audio_win32.h (renamed from plugins/mumble_plugin_win32.h)10
-rw-r--r--plugins/mumble_positional_audio_win32_internals.h (renamed from plugins/mumble_plugin_win32_internals.h)6
-rw-r--r--plugins/null_plugin.cpp3
-rw-r--r--plugins/ql/ql.cpp7
-rw-r--r--plugins/rl/rl.cpp5
-rw-r--r--plugins/se/se.cpp6
-rw-r--r--plugins/sr/sr.cpp5
-rw-r--r--plugins/testPlugin/CMakeLists.txt9
-rw-r--r--plugins/testPlugin/testPlugin.cpp480
-rw-r--r--plugins/ut2004/ut2004.cpp5
-rw-r--r--plugins/ut3/ut3.cpp5
-rw-r--r--plugins/ut99/ut99.cpp7
-rw-r--r--plugins/wolfet/wolfet.cpp5
-rw-r--r--plugins/wow/wow.cpp7
-rw-r--r--plugins/wow_x64/wow_x64.cpp7
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--src/Channel.cpp19
-rw-r--r--src/Channel.h14
-rw-r--r--src/Message.h3
-rw-r--r--src/Mumble.proto13
-rw-r--r--src/MumbleConstants.h20
-rw-r--r--src/ProcessResolver.cpp307
-rw-r--r--src/ProcessResolver.h40
-rw-r--r--src/mumble/API.h188
-rw-r--r--src/mumble/API_v_1_0_x.cpp1980
-rw-r--r--src/mumble/Audio.cpp10
-rw-r--r--src/mumble/AudioInput.cpp21
-rw-r--r--src/mumble/AudioInput.h8
-rw-r--r--src/mumble/AudioOutput.cpp145
-rw-r--r--src/mumble/AudioOutput.h19
-rw-r--r--src/mumble/CMakeLists.txt107
-rw-r--r--src/mumble/ClientUser.cpp4
-rw-r--r--src/mumble/Global.cpp2
-rw-r--r--src/mumble/Global.h5
-rw-r--r--src/mumble/LegacyPlugin.cpp267
-rw-r--r--src/mumble/LegacyPlugin.h82
-rw-r--r--src/mumble/Log.cpp8
-rw-r--r--src/mumble/Log.h10
-rw-r--r--src/mumble/MainWindow.cpp37
-rw-r--r--src/mumble/MainWindow.h8
-rw-r--r--src/mumble/ManualPlugin.cpp39
-rw-r--r--src/mumble/ManualPlugin.h23
-rw-r--r--src/mumble/Messages.cpp21
-rw-r--r--src/mumble/NetworkConfig.cpp11
-rw-r--r--src/mumble/NetworkConfig.ui13
-rw-r--r--src/mumble/Plugin.cpp694
-rw-r--r--src/mumble/Plugin.h417
-rw-r--r--src/mumble/PluginConfig.cpp247
-rw-r--r--src/mumble/PluginConfig.h66
-rw-r--r--src/mumble/PluginConfig.ui (renamed from src/mumble/Plugins.ui)41
-rw-r--r--src/mumble/PluginInstaller.cpp200
-rw-r--r--src/mumble/PluginInstaller.h84
-rw-r--r--src/mumble/PluginInstaller.ui243
-rw-r--r--src/mumble/PluginManager.cpp933
-rw-r--r--src/mumble/PluginManager.h279
-rw-r--r--src/mumble/PluginUpdater.cpp379
-rw-r--r--src/mumble/PluginUpdater.h107
-rw-r--r--src/mumble/PluginUpdater.ui224
-rw-r--r--src/mumble/Plugins.cpp792
-rw-r--r--src/mumble/Plugins.h91
-rw-r--r--src/mumble/PositionalData.cpp242
-rw-r--r--src/mumble/PositionalData.h171
-rw-r--r--src/mumble/ServerHandler.cpp18
-rw-r--r--src/mumble/ServerHandler.h10
-rw-r--r--src/mumble/Settings.cpp86
-rw-r--r--src/mumble/Settings.h16
-rw-r--r--src/mumble/UserModel.cpp11
-rw-r--r--src/mumble/UserModel.h21
-rw-r--r--src/mumble/main.cpp97
-rw-r--r--src/mumble/mumble_ar.ts191
-rw-r--r--src/mumble/mumble_bg.ts193
-rw-r--r--src/mumble/mumble_br.ts191
-rw-r--r--src/mumble/mumble_ca.ts191
-rw-r--r--src/mumble/mumble_cs.ts203
-rw-r--r--src/mumble/mumble_cy.ts193
-rw-r--r--src/mumble/mumble_da.ts203
-rw-r--r--src/mumble/mumble_de.ts203
-rw-r--r--src/mumble/mumble_el.ts203
-rw-r--r--src/mumble/mumble_en.ts191
-rw-r--r--src/mumble/mumble_en_GB.ts191
-rw-r--r--src/mumble/mumble_eo.ts193
-rw-r--r--src/mumble/mumble_es.ts203
-rw-r--r--src/mumble/mumble_et.ts195
-rw-r--r--src/mumble/mumble_eu.ts201
-rw-r--r--src/mumble/mumble_fa_IR.ts191
-rw-r--r--src/mumble/mumble_fi.ts203
-rw-r--r--src/mumble/mumble_fr.ts203
-rw-r--r--src/mumble/mumble_gl.ts191
-rw-r--r--src/mumble/mumble_he.ts204
-rw-r--r--src/mumble/mumble_hu.ts203
-rw-r--r--src/mumble/mumble_it.ts257
-rw-r--r--src/mumble/mumble_ja.ts203
-rw-r--r--src/mumble/mumble_ko.ts203
-rw-r--r--src/mumble/mumble_lt.ts199
-rw-r--r--src/mumble/mumble_nl.ts258
-rw-r--r--src/mumble/mumble_no.ts203
-rw-r--r--src/mumble/mumble_oc.ts191
-rw-r--r--src/mumble/mumble_pl.ts259
-rw-r--r--src/mumble/mumble_pt_BR.ts203
-rw-r--r--src/mumble/mumble_pt_PT.ts203
-rw-r--r--src/mumble/mumble_ro.ts191
-rw-r--r--src/mumble/mumble_ru.ts203
-rw-r--r--src/mumble/mumble_si.ts197
-rw-r--r--src/mumble/mumble_sv.ts259
-rw-r--r--src/mumble/mumble_te.ts191
-rw-r--r--src/mumble/mumble_th.ts191
-rw-r--r--src/mumble/mumble_tr.ts203
-rw-r--r--src/mumble/mumble_uk.ts191
-rw-r--r--src/mumble/mumble_zh_CN.ts252
-rw-r--r--src/mumble/mumble_zh_HK.ts203
-rw-r--r--src/mumble/mumble_zh_TW.ts203
-rw-r--r--src/murmur/Messages.cpp56
-rw-r--r--src/murmur/Meta.cpp6
-rw-r--r--src/murmur/Meta.h3
-rw-r--r--src/murmur/Server.cpp11
-rw-r--r--src/murmur/Server.h3
-rw-r--r--src/murmur/ServerUser.cpp3
-rw-r--r--src/murmur/ServerUser.h1
172 files changed, 17659 insertions, 2070 deletions
diff --git a/.ci/azure-pipelines/install-environment_linux.bash b/.ci/azure-pipelines/install-environment_linux.bash
index 83da24829..e55587a20 100755
--- a/.ci/azure-pipelines/install-environment_linux.bash
+++ b/.ci/azure-pipelines/install-environment_linux.bash
@@ -14,4 +14,5 @@ sudo apt-get -y install build-essential g++-multilib ninja-build pkg-config \
libasound2-dev libasound2-plugins libasound2-plugins-extra\
libogg-dev libsndfile1-dev libspeechd-dev \
libavahi-compat-libdnssd-dev libzeroc-ice-dev \
- zsync appstream libgrpc++-dev protobuf-compiler-grpc
+ zsync appstream libgrpc++-dev protobuf-compiler-grpc \
+ libpoco-dev
diff --git a/.cirrus.yml b/.cirrus.yml
index 5fc851fd2..18f5a7450 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -8,7 +8,7 @@ freebsd_instance:
freebsd_task:
pkg_script:
- pkg update && pkg upgrade -y
- - pkg install -y git ninja pkgconf cmake qt5-buildtools qt5-qmake qt5-linguisttools qt5-concurrent qt5-network qt5-xml qt5-sql qt5-svg qt5-testlib boost-libs libsndfile protobuf ice avahi-libdns grpc
+ - pkg install -y git ninja pkgconf cmake qt5-buildtools qt5-qmake qt5-linguisttools qt5-concurrent qt5-network qt5-xml qt5-sql qt5-svg qt5-testlib boost-libs libsndfile protobuf ice avahi-libdns grpc poco
fetch_submodules_script: git submodule --quiet update --init --recursive
build_script:
- mkdir build && cd build
diff --git a/.github/actions/install-dependencies/install_ubuntu_shared_64bit.sh b/.github/actions/install-dependencies/install_ubuntu_shared_64bit.sh
index f185ddb90..1765eb290 100755
--- a/.github/actions/install-dependencies/install_ubuntu_shared_64bit.sh
+++ b/.github/actions/install-dependencies/install_ubuntu_shared_64bit.sh
@@ -29,4 +29,5 @@ sudo apt -y install \
zsync \
appstream \
libgrpc++-dev \
- protobuf-compiler-grpc
+ protobuf-compiler-grpc \
+ libpoco-dev
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 79249dd35..af4d35e95 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -30,6 +30,7 @@ project(Mumble
)
set(3RDPARTY_DIR "${CMAKE_SOURCE_DIR}/3rdparty")
+set(PLUGINS_DIR "${CMAKE_SOURCE_DIR}/plugins")
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.9)
diff --git a/docs/dev/build-instructions/cmake_options.md b/docs/dev/build-instructions/cmake_options.md
index 60be98248..ac1952cc9 100644
--- a/docs/dev/build-instructions/cmake_options.md
+++ b/docs/dev/build-instructions/cmake_options.md
@@ -149,6 +149,16 @@ Build 32 bit overlay library, necessary for the overlay to work with 32 bit proc
Build package.
(Default: OFF)
+### plugin-callback-debug
+
+Build Mumble with debug output for plugin callbacks inside of Mumble.
+(Default: OFF)
+
+### plugin-debug
+
+Build Mumble with debug output for plugin developers.
+(Default: OFF)
+
### plugins
Build plugins.
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index 41a45c6d4..e15043c85 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -30,6 +30,11 @@ foreach(ITEM ${ITEMS})
# PLUGIN_RETRACTED variable in the parent scope so that we can access it here
add_subdirectory(${ITEM})
+ if(${ITEM} STREQUAL "testPlugin" AND NOT ${CMAKE_BUILD_TYPE} MATCHES Debug)
+ # The testPlugin is only included in Debug builds
+ continue()
+ endif()
+
if(PLUGIN_RETRACTED AND NOT retracted-plugins)
# The included subdir didn't actually add a target since the associated plugin is retracted
# and therefore it should not be built.
diff --git a/plugins/HostLinux.cpp b/plugins/HostLinux.cpp
index 8d53155a5..58feb673e 100644
--- a/plugins/HostLinux.cpp
+++ b/plugins/HostLinux.cpp
@@ -5,7 +5,7 @@
#include "HostLinux.h"
-#include "mumble_plugin_utils.h"
+#include "mumble_positional_audio_utils.h"
#include <cstring>
#include <sstream>
diff --git a/plugins/HostWindows.cpp b/plugins/HostWindows.cpp
index c354b4657..1c0574fc5 100644
--- a/plugins/HostWindows.cpp
+++ b/plugins/HostWindows.cpp
@@ -5,7 +5,7 @@
#include "HostWindows.h"
-#include "mumble_plugin_utils.h"
+#include "mumble_positional_audio_utils.h"
#include <windows.h>
#include <tlhelp32.h>
diff --git a/plugins/MumbleAPI_v_1_0_x.h b/plugins/MumbleAPI_v_1_0_x.h
new file mode 100644
index 000000000..34b178f87
--- /dev/null
+++ b/plugins/MumbleAPI_v_1_0_x.h
@@ -0,0 +1,492 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+/// This header file contains the definition of Mumble's API
+
+#ifndef MUMBLE_PLUGIN_API_H_
+#define MUMBLE_PLUGIN_API_H_
+
+#include "PluginComponents_v_1_0_x.h"
+#include <stdint.h>
+
+
+// API version
+#define MUMBLE_PLUGIN_API_MAJOR_MACRO 1
+#define MUMBLE_PLUGIN_API_MINOR_MACRO 0
+#define MUMBLE_PLUGIN_API_PATCH_MACRO 0
+
+constexpr int32_t MUMBLE_PLUGIN_API_MAJOR = MUMBLE_PLUGIN_API_MAJOR_MACRO;
+constexpr int32_t MUMBLE_PLUGIN_API_MINOR = MUMBLE_PLUGIN_API_MINOR_MACRO;
+constexpr int32_t MUMBLE_PLUGIN_API_PATCH = MUMBLE_PLUGIN_API_PATCH_MACRO;
+const mumble_version_t MUMBLE_PLUGIN_API_VERSION = { MUMBLE_PLUGIN_API_MAJOR, MUMBLE_PLUGIN_API_MINOR, MUMBLE_PLUGIN_API_PATCH };
+
+// Create macro for casting the pointer to the API object to the proper struct.
+// Note that this must only be used if the API uses MUMBLE_PLUGIN_API_VERSION of the API.
+#define MUMBLE_CONCAT_HELPER(a, b) a ## _ ## b
+#define MUMBLE_CONCAT(a, b) MUMBLE_CONCAT_HELPER(a, b)
+#define MUMBLE_API_STRUCT MUMBLE_CONCAT(MumbleAPI_v, MUMBLE_CONCAT(MUMBLE_PLUGIN_API_MAJOR_MACRO, MUMBLE_CONCAT(MUMBLE_PLUGIN_API_MINOR_MACRO, x)))
+#define MUMBLE_API_CAST(ptrName) ( *((struct MUMBLE_API_STRUCT *) ptrName) )
+
+
+struct MumbleAPI_v_1_0_x {
+ /////////////////////////////////////////////////////////
+ ////////////////////// GENERAL NOTES ////////////////////
+ /////////////////////////////////////////////////////////
+ //
+ // All functions that take in a connection as a paremeter may only be called **after** the connection
+ // has finished synchronizing. The only exception from this is isConnectionSynchronized.
+ //
+ // Strings returned by the API are UTF-8 encoded
+ // Strings passed to the API are expected to be UTF-8 encoded
+ //
+ // All API functions are synchronized and will be executed in Mumble's "main thread" from which most plugin
+ // callbacks are called as well. Note however that an API call is BLOCKING if invoked from a different
+ // thread. This means that they can cause deadlocks if used without caution. An example that will lead
+ // to a deadlock is:
+ // - plugin callback gets called from the main thread
+ // - callback messages a separate thread to do something and waits for the action to have completed
+ // - Separate thread calls an API function
+ // - The function blocks and waits to be executed in the main thread which is currently blocked waiting
+ // - deadlock
+
+
+ // -------- Memory management --------
+
+ /// Frees the given pointer.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param pointer The pointer to free
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *freeMemory)(mumble_plugin_id_t callerID, const void *pointer);
+
+
+
+ // -------- Getter functions --------
+
+ /// Gets the connection ID of the server the user is currently active on (the user's audio output is directed at).
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param[out] connection A pointer to the memory location the ID should be written to
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then it is valid to access the
+ /// value of the provided pointer
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getActiveServerConnection)(mumble_plugin_id_t callerID, mumble_connection_t *connection);
+
+ /// Checks whether the given connection has finished initializing yet.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param[out] A pointer to the boolean variable that'll hold the info whether the server has finished synchronization yet
+ /// after this function has executed successfully.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *isConnectionSynchronized)(mumble_plugin_id_t callerID, mumble_connection_t connection, bool *synchronized);
+
+ /// Fills in the information about the local user.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param[out] userID A pointer to the memory the user's ID shall be written to
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getLocalUserID)(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t *userID);
+
+ /// Fills in the information about the given user's name.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param userID The user's ID whose name should be obtained
+ /// @param[out] userName A pointer to where the pointer to the allocated string (C-encoded) should be written to. The
+ /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be
+ /// allocated if this function returns STATUS_OK.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getUserName)(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, const char **userName);
+
+ /// Fills in the information about the given channel's name.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param channelID The channel's ID whose name should be obtained
+ /// @param[out] channelName A pointer to where the pointer to the allocated string (C-ecoded) should be written to. The
+ /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be
+ /// allocated if this function returns STATUS_OK.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getChannelName)(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **channelName);
+
+ /// Gets an array of all users that are currently connected to the provided server. Passing a nullptr as any of the out-parameter
+ /// will prevent that property to be set/allocated. If you are only interested in the user count you can thus pass nullptr as the
+ /// users parameter and save time on allocating + freeing the channels-array while still getting the size out.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param[out] users A pointer to where the pointer of the allocated array shall be written. The
+ /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be
+ /// allocated if this function returns STATUS_OK.
+ /// @param[out] userCount A pointer to where the size of the allocated user-array shall be written to
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getAllUsers)(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t **users,
+ size_t *userCount);
+
+ /// Gets an array of all channels on the provided server. Passing a nullptr as any of the out-parameter will prevent
+ /// that property to be set/allocated. If you are only interested in the channel count you can thus pass nullptr as the
+ /// channels parameter and save time on allocating + freeing the channels-array while still getting the size out.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param[out] channels A pointer to where the pointer of the allocated array shall be written. The
+ /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be
+ /// allocated if this function returns STATUS_OK.
+ /// @param[out] channelCount A pointer to where the size of the allocated channel-array shall be written to
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getAllChannels)(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t **channels, size_t *channelCount);
+
+ /// Gets the ID of the channel the given user is currently connected to.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param userID The ID of the user to search for
+ /// @param[out] A pointer to where the ID of the channel shall be written
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getChannelOfUser)(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ mumble_channelid_t *channel);
+
+ /// Gets an array of all users in the specified channel.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param channelID The ID of the channel whose users shall be retrieved
+ /// @param[out] userList A pointer to where the pointer of the allocated array shall be written. The allocated memory has
+ /// to be freed by a call to freeMemory by the plugin eventually. The memory will only be allocated if this function
+ /// returns STATUS_OK.
+ /// @param[out] userCount A pointer to where the size of the allocated user-array shall be written to
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getUsersInChannel)(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, mumble_userid_t **userList, size_t *userCount);
+
+ /// Gets the current transmission mode of the local user.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param[out] transmissionMode A pointer to where the transmission mode shall be written.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getLocalUserTransmissionMode)(mumble_plugin_id_t callerID, mumble_transmission_mode_t *transmissionMode);
+
+ /// Checks whether the given user is currently locally muted.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param userID The ID of the user to check for
+ /// @param[out] muted A pointer to where the local mute state of that user shall be written
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *isUserLocallyMuted)(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, bool *muted);
+
+ /// Checks whether the local user is currently muted.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param[out] muted A pointer to where the mute state of the local user shall be written
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *isLocalUserMuted)(mumble_plugin_id_t callerID, bool *muted);
+
+ /// Checks whether the local user is currently deafened.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param[out] deafened A pointer to where the deaf state of the local user shall be written
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *isLocalUserDeafened)(mumble_plugin_id_t callerID, bool *deafened);
+
+ /// Gets the hash of the given user (can be used to recognize users between restarts)
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param userID The ID of the user to search for
+ /// @param[out] hash A pointer to where the pointer to the allocated string (C-encoded) should be written to. The
+ /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be
+ /// allocated if this function returns STATUS_OK.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getUserHash)(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, const char **hash);
+
+ /// Gets the hash of the server for the given connection (can be used to recognize servers between restarts)
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection
+ /// @param[out] hash A pointer to where the pointer to the allocated string (C-encoded) should be written to. The
+ /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be
+ /// allocated if this function returns STATUS_OK.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getServerHash)(mumble_plugin_id_t callerID, mumble_connection_t connection, const char **hash);
+
+ /// Gets the comment of the given user. Note that a user might have a comment configured that hasn't been synchronized
+ /// to this client yet. In this case this function will return EC_UNSYNCHRONIZED_BLOB. As of now there is now way
+ /// to request the synchronization to happen via the Plugin-API.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection
+ /// @param userID the ID of the user whose comment should be obtained
+ /// @param[out] comment A pointer to where the pointer to the allocated string (C-encoded) should be written to. The
+ /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be
+ /// allocated if this function returns STATUS_OK.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getUserComment)(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, const char **comment);
+
+ /// Gets the description of the given channel. Note that a channel might have a description configured that hasn't been synchronized
+ /// to this client yet. In this case this function will return EC_UNSYNCHRONIZED_BLOB. As of now there is now way
+ /// to request the synchronization to happen via the Plugin-API.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection
+ /// @param channelID the ID of the channel whose comment should be obtained
+ /// @param[out] description A pointer to where the pointer to the allocated string (C-encoded) should be written to. The
+ /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be
+ /// allocated if this function returns STATUS_OK.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getChannelDescription)(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **description);
+
+
+ // -------- Request functions --------
+
+ /// Requests Mumble to set the local user's transmission mode to the specified one. If you only need to temporarily set
+ /// the transmission mode to continous, use requestMicrophoneActivationOverwrite instead as this saves you the work of
+ /// restoring the previous state afterwards.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param transmissionMode The requested transmission mode
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *requestLocalUserTransmissionMode)(mumble_plugin_id_t callerID, mumble_transmission_mode_t transmissionMode);
+
+ /// Requests Mumble to move the given user into the given channel
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param userID The ID of the user that shall be moved
+ /// @param channelID The ID of the channel to move the user to
+ /// @param password The password of the target channel (UTF-8 encoded as a C-string). Pass NULL if the target channel does not require a
+ /// password for entering
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *requestUserMove)(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ mumble_channelid_t channelID, const char *password);
+
+ /// Requests Mumble to overwrite the microphone activation so that the microphone is always on (same as if the user had chosen
+ /// the continous transmission mode). If a plugin requests this overwrite, it is responsible for deactivating the overwrite again
+ /// once it is no longer required
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param activate Whether to activate the overwrite (false deactivates an existing overwrite)
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *requestMicrophoneActivationOvewrite)(mumble_plugin_id_t callerID, bool activate);
+
+ /// Requests Mumble to set the local mute state of the given client. Note that this only affects the **local** mute state
+ /// opposed to a server-mute (client is globally muted by the server) or the client's own mute-state (client has muted its
+ /// microphone and thus isn't transmitting any audio).
+ /// Furthermore it must be noted that muting the local user with this function does not work (it doesn't make sense). If
+ /// you try to do so, this function will fail. In order to make this work, this function will also fail if the server
+ /// has not finished synchronizing with the client yet.
+ /// For muting the local user, use requestLocalUserMute instead.
+ ///
+ /// @param callerID The ID of the plugin calling this function.
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param userID The ID of the user that shall be muted
+ /// @param muted Whether to locally mute the given client (opposed to unmuting it)
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *requestLocalMute)(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, bool muted);
+
+ /// Requests Mumble to set the mute state of the local user. In the UI this is referred to as "self-mute".
+ ///
+ /// @param callerID The ID of the plugin calling this function.
+ /// @param muted Whether to locally mute the local user (opposed to unmuting it)
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *requestLocalUserMute)(mumble_plugin_id_t callerID, bool muted);
+
+ /// Requests Mumble to set the deaf state of the local user. In the UI this is referred to as "self-deaf".
+ ///
+ /// @param callerID The ID of the plugin calling this function.
+ /// @param deafened Whether to locally deafen the local user (opposed to undeafening it)
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *requestLocalUserDeaf)(mumble_plugin_id_t callerID, bool deafened);
+
+ /// Sets the comment of the local user
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection
+ /// @param comment The new comment to use (C-encoded). A subset of HTML formatting is supported.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer
+ /// may be accessed
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *requestSetLocalUserComment)(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const char *comment);
+
+
+
+ // -------- Find functions --------
+
+ /// Fills in the information about a user with the specified name, if such a user exists. The search is case-sensitive.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param userName The respective user's name
+ /// @param[out] userID A pointer to the memory the user's ID shall be written to
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may
+ /// be accessed.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *findUserByName)(mumble_plugin_id_t callerID, mumble_connection_t connection, const char *userName,
+ mumble_userid_t *userID);
+
+ /// Fills in the information about a channel with the specified name, if such a channel exists. The search is case-sensitive.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to use as a context
+ /// @param channelName The respective channel's name
+ /// @param[out] channelID A pointer to the memory the channel's ID shall be written to
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may
+ /// be accessed.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *findChannelByName)(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const char *channelName, mumble_channelid_t *channelID);
+
+
+
+ // -------- Settings --------
+
+ /// Fills in the current value of the setting with the given key. Note that this function can only be used for settings whose value
+ /// is a bool!
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param key The key to the desired setting
+ /// @param[out] outValue A pointer to the memory the setting's value shall be written to.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may
+ /// be accessed.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getMumbleSetting_bool)(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool *outValue);
+
+ /// Fills in the current value of the setting with the given key. Note that this function can only be used for settings whose value
+ /// is an int!
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param key The key to the desired setting
+ /// @param[out] outValue A pointer to the memory the setting's value shall be written to.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may
+ /// be accessed.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getMumbleSetting_int)(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t *outValue);
+
+ /// Fills in the current value of the setting with the given key. Note that this function can only be used for settings whose value
+ /// is a double!
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param key The key to the desired setting
+ /// @param[out] outValue A pointer to the memory the setting's value shall be written to.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may
+ /// be accessed.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getMumbleSetting_double)(mumble_plugin_id_t callerID, mumble_settings_key_t key, double *outValue);
+
+ /// Fills in the current value of the setting with the given key. Note that this function can only be used for settings whose value
+ /// is a String!
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param key The key to the desired setting
+ /// @param[out] outValue The memory address to which the pointer to the setting's value (the String) will be written. The
+ /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be
+ /// allocated if this function returns STATUS_OK.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may
+ /// be accessed.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *getMumbleSetting_string)(mumble_plugin_id_t callerID, mumble_settings_key_t key, const char **outValue);
+
+
+ /// Sets the value of the setting with the given key. Note that this function can only be used for settings whose value
+ /// is a bool!
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param key The key to the desired setting
+ /// @param value The value that should be set for the given setting
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *setMumbleSetting_bool)(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool value);
+
+ /// Sets the value of the setting with the given key. Note that this function can only be used for settings whose value
+ /// is an int!
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param key The key to the desired setting
+ /// @param value The value that should be set for the given setting
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *setMumbleSetting_int)(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t value);
+
+ /// Sets the value of the setting with the given key. Note that this function can only be used for settings whose value
+ /// is a double!
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param key The key to the desired setting
+ /// @param value The value that should be set for the given setting
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *setMumbleSetting_double)(mumble_plugin_id_t callerID, mumble_settings_key_t key, double value);
+
+ /// Sets the value of the setting with the given key. Note that this function can only be used for settings whose value
+ /// is a string!
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param key The key to the desired setting
+ /// @param value The value that should be set for the given setting
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *setMumbleSetting_string)(mumble_plugin_id_t callerID, mumble_settings_key_t key, const char *value);
+
+
+
+
+ // -------- Miscellaneous --------
+
+ /// Sends the provided data to the provided client(s). This kind of data can only be received by another plugin active
+ /// on that client. The sent data can be seen by any active plugin on the receiving client. Therefore the sent data
+ /// must not contain sensitive information or anything else that shouldn't be known by others.
+ ///
+ /// NOTE: Messages sent via this API function are rate-limited by the server. If the rate-limit is hit, the message
+ /// will be dropped without an error message. The rate-limiting is global (e.g. it doesn't matter which plugin sent
+ /// the respective messages - they all count to the same limit).
+ /// Therefore if you have multiple messages to send, you should consider sending them asynchronously one at a time
+ /// with a little delay in between (~1 second).
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param connection The ID of the server-connection to send the data through (the server the given users are on)
+ /// @param users An array of user IDs to send the data to
+ /// @param userCount The size of the provided user-array
+ /// @param data The data array that shall be sent. This can be an arbitrary sequence of bytes. Note that the size of
+ /// is restricted to <= 1KB.
+ /// @param dataLength The length of the data array
+ /// @param dataID The ID of the sent data. This has to be used by the receiving plugin(s) to figure out what to do with
+ /// the data. This has to be a C-encoded String. It is recommended that the ID starts with a plugin-specific prefix
+ /// in order to avoid name clashes. Note that the size of this string is restricted to <= 100 bytes.
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *sendData)(mumble_plugin_id_t callerID, mumble_connection_t connection, const mumble_userid_t *users,
+ size_t userCount, const uint8_t *data, size_t dataLength, const char *dataID);
+
+ /// Logs the given message (typically to Mumble's console). All passed strings have to be UTF-8 encoded.
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param message The message to log
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *log)(mumble_plugin_id_t callerID, const char *message);
+
+ /// Plays the provided sample. It uses libsndfile as a backend so the respective file format needs to be supported by it
+ /// in order for this to work out (see http://www.mega-nerd.com/libsndfile/).
+ ///
+ /// @param callerID The ID of the plugin calling this function
+ /// @param samplePath The path to the sample that shall be played (UTF-8 encoded)
+ /// @returns The error code. If everything went well, STATUS_OK will be returned.
+ mumble_error_t (PLUGIN_CALLING_CONVENTION *playSample)(mumble_plugin_id_t callerID, const char *samplePath);
+};
+
+#endif
diff --git a/plugins/MumblePlugin_v_1_0_x.h b/plugins/MumblePlugin_v_1_0_x.h
new file mode 100644
index 000000000..f3feb740b
--- /dev/null
+++ b/plugins/MumblePlugin_v_1_0_x.h
@@ -0,0 +1,402 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+/// This header file specifies the Mumble plugin interface
+
+#ifndef EXTERNAL_MUMBLE_PLUGIN_H_
+#define EXTERNAL_MUMBLE_PLUGIN_H_
+
+#include "PluginComponents_v_1_0_x.h"
+#include "MumbleAPI_v_1_0_x.h"
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+#if defined(__GNUC__) && !defined(__MINGW32__) // GCC on Unix-like systems
+ #define PLUGIN_EXPORT __attribute__((visibility("default")))
+#elif defined(_MSC_VER)
+ #define PLUGIN_EXPORT __declspec(dllexport)
+#elif defined(__MINGW32__)
+ #define PLUGIN_EXPORT __attribute__((dllexport))
+#else
+ #error No PLUGIN_EXPORT definition available
+#endif
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+ //////////////////////////////////////////////////////////////////////////////////
+ ////////////////////////////// MANDATORY FUNCTIONS ///////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////////
+
+ /// Gets called right after loading the plugin in order to let the plugin initialize.
+ ///
+ /// Registers the ID of this plugin.
+ /// @param id The ID for this plugin. This is the ID Mumble will reference this plugin with
+ /// and by which this plugin can identify itself when communicating with Mumble.
+ /// @returns The status of the initialization. If everything went fine, return STATUS_OK
+ PLUGIN_EXPORT mumble_error_t PLUGIN_CALLING_CONVENTION mumble_init(uint32_t id);
+
+ /// Gets called when unloading the plugin in order to allow it to clean up after itself.
+ /// Note that it is still safe to call API functions from within this callback.
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_shutdown();
+
+ /// Gets the name of the plugin.
+ ///
+ /// NOTE: This function may be called without the plugin being loaded
+ ///
+ /// @returns A String-wrapper containing the requested name
+ PLUGIN_EXPORT struct MumbleStringWrapper PLUGIN_CALLING_CONVENTION mumble_getName();
+
+ /// Gets the Version of the plugin-API this plugin intends to use.
+ /// Mumble will decide whether this plugin is loadable or not based on the return value of this function.
+ ///
+ /// NOTE: This function may be called without the plugin being loaded
+ ///
+ /// @returns The respective API Version
+ PLUGIN_EXPORT mumble_version_t PLUGIN_CALLING_CONVENTION mumble_getAPIVersion();
+
+ /// Provides the MumbleAPI struct to the plugin. This struct contains function pointers that can be used
+ /// to interact with the Mumble client. It is up to the plugin to store this struct somewhere if it wants to make use
+ /// of it at some point.
+ ///
+ /// NOTE: This function may be called without the plugin being loaded
+ ///
+ /// @param api A pointer to the MumbleAPI struct. The API struct must be cast to the version corresponding to the
+ /// user API version. If your plugin is e.g. using the 1.0.x API, then you have to cast this pointer to
+ /// MumbleAPI_v_1_0_x. Note also that you **must not store this pointer**. It will become invalid. Therefore
+ /// you have to copy the struct in order to use it later on.
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_registerAPIFunctions(void *apiStruct);
+
+ /// Releases the resource pointed to by the given pointer. If the respective resource has been allocated before,
+ /// this would be the time to free/delete it.
+ /// The resources processed by this functions are only those that have been specifically allocated in order to return
+ /// them in one of the plugin functions to Mumble (e.g. the String returned by mumble_getName) and has nothing to do
+ /// with your plugin's internal resource management.
+ /// In short: Only resources passed from the plugin to Mumble via a return value may be processed by this function.
+ ///
+ /// NOTE1: This function may be called without the plugin being loaded
+ ///
+ /// NOTE2: that the pointer might be pointing to memory that had to be allocated without the plugin being loaded.
+ /// Therefore you should be very sure that there'll be another callback in which you want to free this memory,
+ /// should you decide to not do it here (which is hereby explicitly advised against).
+ ///
+ /// NOTE3: The pointer is const as Mumble won't mess with the memory allocated by the plugin (no modifications).
+ /// Nontheless this function is explicitly responsible for freeing the respective memory parts. If the memory has
+ /// been allocated using malloc(), it needs to be freed using free() which requires a const-cast. If however the
+ /// memory has been created using the new operator you have to cast the pointer back to its original type and then
+ /// use the delete operator on it (no const-cast necessary in this case).
+ /// See https://stackoverflow.com/questions/2819535/unable-to-free-const-pointers-in-c
+ /// and https://stackoverflow.com/questions/941832/is-it-safe-to-delete-a-void-pointer
+ ///
+ /// @param pointer The pointer to the memory that needs free-ing
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_releaseResource(const void *pointer);
+
+
+
+ //////////////////////////////////////////////////////////////////////////////////
+ ///////////////////////////// GENERAL FUNCTIONS //////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////////
+
+ /// Tells the plugin some basic information about the Mumble client loading it.
+ /// This function will be the first one that is being called on this plugin - even before it is decided whether to load
+ /// the plugin at all.
+ ///
+ /// @param mumbleVersion The Version of the Mumble client
+ /// @param mumbleAPIVersion The Version of the plugin-API the Mumble client runs with
+ /// @param minimumExpectedAPIVersion The minimum Version the Mumble clients expects this plugin to meet in order to load it
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_setMumbleInfo(mumble_version_t mumbleVersion, mumble_version_t mumbleAPIVersion, mumble_version_t minimumExpectedAPIVersion);
+
+ /// Gets the Version of this plugin
+ ///
+ /// NOTE: This function may be called without the plugin being loaded
+ ///
+ /// @returns The plugin's version
+ PLUGIN_EXPORT mumble_version_t PLUGIN_CALLING_CONVENTION mumble_getVersion();
+
+ /// Gets the name of the plugin author(s).
+ ///
+ /// NOTE: This function may be called without the plugin being loaded
+ ///
+ /// @returns A String-wrapper containing the requested author name(s)
+ PLUGIN_EXPORT struct MumbleStringWrapper PLUGIN_CALLING_CONVENTION mumble_getAuthor();
+
+ /// Gets the description of the plugin.
+ ///
+ /// NOTE: This function may be called without the plugin being loaded
+ ///
+ /// @returns A String-wrapper containing the requested description
+ PLUGIN_EXPORT struct MumbleStringWrapper PLUGIN_CALLING_CONVENTION mumble_getDescription();
+
+ /// Gets the feature set of this plugin. The feature set is described by bitwise or'ing the elements of the Mumble_PluginFeature enum
+ /// together.
+ ///
+ /// NOTE: This function may be called without the plugin being loaded
+ ///
+ /// @returns The feature set of this plugin
+ PLUGIN_EXPORT uint32_t PLUGIN_CALLING_CONVENTION mumble_getFeatures();
+
+ /// Requests this plugin to deactivate the given (sub)set of provided features.
+ /// If this is not possible, the features that can't be deactivated shall be returned by this function.
+ ///
+ /// Example (check if FEATURE_POSITIONAL shall be deactivated):
+ /// @code
+ /// if (features & FEATURE_POSITIONAL) {
+ /// // positional shall be deactivated
+ /// };
+ /// @endcode
+ ///
+ /// @param features The feature set that shall be deactivated
+ /// @returns The feature set that can't be disabled (bitwise or'ed). If all requested features can be disabled, return
+ /// FEATURE_NONE. If none of the requested features can be disabled return the unmodified features parameter.
+ PLUGIN_EXPORT uint32_t PLUGIN_CALLING_CONVENTION mumble_deactivateFeatures(uint32_t features);
+
+
+
+ //////////////////////////////////////////////////////////////////////////////////
+ //////////////////////////// POSITIONAL DATA /////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////////
+ // If this plugin wants to provide positional data, ALL functions of this category
+ // have to be implemented
+
+ /// Indicates that Mumble wants to use this plugin to request positional data. Therefore it should check whether it is currently
+ /// able to do so and allocate memory that is needed for that process.
+ /// As a parameter this function gets an array of names and an array of PIDs. They are of same length and the PID at index i
+ /// belongs to a program whose name is listed at index i in the "name-array".
+ ///
+ /// @param programNames An array of pointers to the program names
+ /// @param programPIDs An array of the corresponding program PIDs
+ /// @param programCount The length of programNames and programPIDs
+ /// @returns The error code. If everything went fine PDEC_OK shall be returned. In that case Mumble will start frequently
+ /// calling fetchPositionalData. If this returns anything but PDEC_OK, Mumble will assume that the plugin is (currently)
+ /// uncapable of providing positional data. In this case this function must not have allocated any memory that needs to be
+ /// cleaned up later on. Depending on the returned error code, Mumble might try to call this function again at some point.
+ PLUGIN_EXPORT uint8_t PLUGIN_CALLING_CONVENTION mumble_initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount);
+
+ /// Retrieves the positional data. If no data can be fetched, set all float-vectors to 0 and return false.
+ ///
+ /// @param[out] avatarPos A float-array of size 3 representing the cartesian position of the player/avatar in the ingame world.
+ /// One unit represents one meter of distance.
+ /// @param[out] avatarDir A float-array of size 3 representing the cartesian direction-vector of the player/avatar ingame (where it
+ /// is facing).
+ /// @param[out] avatarAxis A float-array of size 3 representing the vector pointing from the toes of the character to its head. One
+ /// unit represents one meter of distance.
+ /// @param[out] cameraPos A float-array of size 3 representing the cartesian position of the camera in the ingame world.
+ /// One unit represents one meter of distance.
+ /// @param[out] cameraDir A float-array of size 3 representing the cartesian direction-vector of the camera ingame (where it
+ /// is facing).
+ /// @param[out] cameraAxis A float-array of size 3 representing a vector from the bottom of the camera to its top. One unit
+ /// represents one meter of distance.
+ /// @param[out] context A pointer to where the pointer to a C-encoded string storing the context of the provided positional data
+ /// shall be written. This context should include information about the server (and team) the player is on. Only players with identical
+ /// context will be able to hear each other's audio. The returned pointer has to remain valid until the next invokation of this function
+ /// or until shutdownPositionalData is called.
+ /// @param[out] identity A pointer to where the pointer to a C-encoded string storing the identity of the player shall be written. It can
+ /// be polled by external scripts from the server and should uniquely identify the player in the game. The pointer has to remain valid
+ /// until the next invokation of this function or until shutdownPositionalData is called.
+ /// @returns Whether this plugin can continue delivering positional data. If this function returns false, shutdownPositionalData will
+ /// be called.
+ PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_fetchPositionalData(float *avatarPos, float *avatarDir, float *avatarAxis, float *cameraPos, float *cameraDir,
+ float *cameraAxis, const char **context, const char **identity);
+
+ /// Indicates that this plugin will not be asked for positional data any longer. Thus any memory allocated for this purpose should
+ /// be freed at this point.
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_shutdownPositionalData();
+
+
+
+ //////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// EVENTHANDLERS / CALLBACK FUNCTIONS ////////////////////////
+ //////////////////////////////////////////////////////////////////////////////////
+
+ /// Called when connecting to a server.
+ /// Note that in most cases you'll want to use mumble_onServerSynchronized instead.
+ /// Note also that this callback will be called from a DIFFERENT THREAD!
+ ///
+ /// @param connection The ID of the newly established server-connection
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onServerConnected(mumble_connection_t connection);
+
+ /// Called when disconnecting from a server.
+ /// Note that this callback is called from a DIFFERENT THREAD!
+ ///
+ /// @param connection The ID of the server-connection that has been terminated
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onServerDisconnected(mumble_connection_t connection);
+
+ /// Called when the client has finished synchronizing with the server
+ ///
+ /// @param connection The ID of the server-connection that has been terminated
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onServerSynchronized(mumble_connection_t connection);
+
+ /// Called whenever any user on the server enters a channel
+ /// This function will also be called when freshly connecting to a server as each user on that
+ /// server needs to be "added" to the respective channel as far as the local client is concerned.
+ ///
+ /// @param connection The ID of the server-connection this event is connected to
+ /// @param userID The ID of the user this event has been triggered for
+ /// @param previousChannelID The ID of the chanel the user is coming from. Negative IDs indicate that there is no previous channel (e.g. the user
+ /// freshly connected to the server) or the channel isn't available because of any other reason.
+ /// @param newChannelID The ID of the channel the user has entered. If the ID is negative, the new channel could not be retrieved. This means
+ /// that the ID is invalid.
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onChannelEntered(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t previousChannelID,
+ mumble_channelid_t newChannelID);
+
+ /// Called whenever a user leaves a channel.
+ /// This includes a client disconnecting from the server as this will also lead to the user not being in that channel anymore.
+ ///
+ /// @param connection The ID of the server-connection this event is connected to
+ /// @param userID The ID of the user that left the channel
+ /// @param channelID The ID of the channel the user left. If the ID is negative, the channel could not be retrieved. This means that the ID is
+ /// invalid.
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onChannelExited(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t channelID);
+
+ /// Called when any user changes his/her talking state.
+ ///
+ /// @param connection The ID of the server-connection this event is connected to
+ /// @param userID The ID of the user whose talking state has been changed
+ /// @param talkingState The new TalkingState the user has switched to.
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onUserTalkingStateChanged(mumble_connection_t connection, mumble_userid_t userID, mumble_talking_state_t talkingState);
+
+ /// Called whenever there is audio input.
+ /// Note that this callback will be called from the AUDIO THREAD.
+ /// Note also that blocking this callback will cause Mumble's audio processing to get suspended.
+ ///
+ /// @param inputPCM A pointer to a short-array holding the pulse-code-modulation (PCM) representing the audio input. Its length
+ /// is sampleCount * channelCount. The PCM format for stereo input is [LRLRLR...] where L and R are samples of the left and right
+ /// channel respectively.
+ /// @param sampleCount The amount of sample points per channel
+ /// @param channelCount The amount of channels in the audio
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech A boolean flag indicating whether Mumble considers the input as part of speech (instead of background noise)
+ /// @returns Whether this callback has modified the audio input-array
+ PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_onAudioInput(short *inputPCM, uint32_t sampleCount, uint16_t channelCount,
+ uint32_t sampleRate, bool isSpeech);
+
+ /// Called whenever Mumble fetches data from an active audio source (could be a voice packet or a playing sample).
+ /// The provided audio buffer is the raw buffer without any processing applied to it yet.
+ /// Note that this callback will be called from the AUDIO THREAD.
+ /// Note also that blocking this callback will cause Mumble's audio processing to get suspended.
+ ///
+ /// @param outputPCM A pointer to a float-array holding the pulse-code-modulation (PCM) representing the audio output. Its length
+ /// is sampleCount * channelCount. The PCM format for stereo output is [LRLRLR...] where L and R are samples of the left and right
+ /// channel respectively.
+ /// @param sampleCount The amount of sample points per channel
+ /// @param channelCount The amount of channels in the audio
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether this audio belongs to a received voice packet (and will thus (most likely) contain speech)
+ /// @param userID If isSpeech is true, this contains the ID of the user this voice packet belongs to. If isSpeech is false,
+ /// the content of this parameter is unspecified and should not be accessed
+ /// @returns Whether this callback has modified the audio output-array
+ PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_onAudioSourceFetched(float *outputPCM, uint32_t sampleCount, uint16_t channelCount,
+ uint32_t sampleRate, bool isSpeech, mumble_userid_t userID);
+
+ /// Called whenever the fully mixed and processed audio is about to be handed to the audio backend (about to be played).
+ /// Note that this happens immediately before Mumble clips the audio buffer.
+ /// Note that this callback will be called from the AUDIO THREAD.
+ /// Note also that blocking this callback will cause Mumble's audio processing to get suspended.
+ ///
+ /// @param outputPCM A pointer to a float-array holding the pulse-code-modulation (PCM) representing the audio output. Its length
+ /// is sampleCount * channelCount. The PCM format for stereo output is [LRLRLR...] where L and R are samples of the left and right
+ /// channel respectively.
+ /// @param sampleCount The amount of sample points per channel
+ /// @param channelCount The amount of channels in the audio
+ /// @param sampleRate The used sample rate in Hz
+ /// @returns Whether this callback has modified the audio output-array
+ PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_onAudioOutputAboutToPlay(float *outputPCM, uint32_t sampleCount, uint16_t channelCount,
+ uint32_t sampleRate);
+
+ /// Called whenever data has been received that has been sent by a plugin. This data should only be processed by the
+ /// intended plugin. For this reason a dataID is provided that should be used to determine whether the data is intended
+ /// for this plugin or not. As soon as the data has been processed, no further plugins will be notified about it.
+ ///
+ /// @param connection The ID of the server-connection the data is coming from
+ /// @param sender The ID of the user whose client's plugin has sent the data
+ /// @param data The sent data array. This can be an arbitrary sequence of bytes.
+ /// @param dataLength The length of the data array
+ /// @param dataID The ID of this data (C-encoded)
+ /// @return Whether the given data has been processed by this plugin
+ PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_onReceiveData(mumble_connection_t connection, mumble_userid_t sender,
+ const uint8_t *data, size_t dataLength, const char *dataID);
+
+ /// Called when a new user gets added to the user model. This is the case when that new user freshly connects to the server the
+ /// local user is on but also when the local user connects to a server other clients are already connected to (in this case this
+ /// method will be called for every client already on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that has been added
+
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onUserAdded(mumble_connection_t connection, mumble_userid_t userID);
+
+ /// Called when a user gets removed from the user model. This is the case when that user disconnects from the server the
+ /// local user is on but also when the local user disconnects from a server other clients are connected to (in this case this
+ /// method will be called for every client on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that has been removed
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onUserRemoved(mumble_connection_t connection, mumble_userid_t userID);
+
+ /// Called when a new channel gets added to the user model. This is the case when a new channel is created on the server the local
+ /// user is on but also when the local user connects to a server that contains channels other than the root-channel (in this case
+ /// this method will be called for ever non-root channel on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param channelID The ID of the channel that has been added
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onChannelAdded(mumble_connection_t connection, mumble_channelid_t channelID);
+
+ /// Called when a channel gets removed from the user model. This is the case when a channel is removed on the server the local
+ /// user is on but also when the local user disconnects from a server that contains channels other than the root-channel (in this case
+ /// this method will be called for ever non-root channel on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param channelID The ID of the channel that has been removed
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onChannelRemoved(mumble_connection_t connection, mumble_channelid_t channelID);
+
+ /// Called when a channel gets renamed. This also applies when a new channel is created (thus assigning it an initial name is also
+ /// considered renaming).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param channelID The ID of the channel that has been renamed
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onChannelRenamed(mumble_connection_t connection, mumble_channelid_t channelID);
+
+ /// Called when a key has been pressed or released while Mumble has keyboard focus.
+ /// Note that this callback will only work if the user has explicitly given permission to monitor keyboard
+ /// events for this plugin. Thus if you want to use this callback, make sure your users know that they have to
+ /// enable that.
+ ///
+ /// @param keyCode The key code of the respective key. The character codes are defined
+ /// via the Mumble_KeyCode enum. For printable 7-bit ASCII characters these codes conform
+ /// to the ASCII code-page with the only difference that case is not distinguished. Therefore
+ /// always the upper-case letter code will be used for letters.
+ /// @param wasPres Whether the respective key has been pressed (instead of released)
+ PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onKeyEvent(uint32_t keyCode, bool wasPress);
+
+
+
+ //////////////////////////////////////////////////////////////////////////////////
+ ////////////////////////////// PLUGIN UPDATES ////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////////
+
+ /// This function is used to determine whether the plugin can find an update for itself that is available for download.
+ ///
+ /// NOTE: This function may be called without the plugin being loaded
+ ///
+ /// @return Whether the plugin was able to find an update for itself
+ PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_hasUpdate();
+
+ /// This function is used to retrieve the URL for downloading the newer/updated version of this plugin.
+ ///
+ /// NOTE: This function may be called without the plugin being loaded
+ ///
+ /// @returns A String-wrapper containing the requested URL
+ PLUGIN_EXPORT struct MumbleStringWrapper PLUGIN_CALLING_CONVENTION mumble_getUpdateDownloadURL();
+
+
+#ifdef __cplusplus
+}
+#endif
+
+
+#endif
diff --git a/plugins/PluginComponents_v_1_0_x.h b/plugins/PluginComponents_v_1_0_x.h
new file mode 100644
index 000000000..628283f22
--- /dev/null
+++ b/plugins/PluginComponents_v_1_0_x.h
@@ -0,0 +1,411 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+/// This header file contains definitions of types and other components used in Mumble's plugin system
+
+#ifndef MUMBLE_PLUGINCOMPONENT_H_
+#define MUMBLE_PLUGINCOMPONENT_H_
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+# include <string>
+#endif
+
+#ifdef QT_VERSION
+ #include <QString>
+#endif
+
+// define the calling convention macro based on the compiler being used
+#if defined(_MSC_VER)
+ #define PLUGIN_CALLING_CONVENTION __cdecl
+#elif defined(__MINGW32__)
+ #define PLUGIN_CALLING_CONVENTION __attribute__((cdecl))
+#else
+ #define PLUGIN_CALLING_CONVENTION
+#endif
+
+
+/// A macro holding the exit status of a successful operation
+#define STATUS_OK EC_OK
+/// A macro holding the version object that is considered to correspond to an unknown version
+#define VERSION_UNKNOWN Version({0,0,0})
+
+
+/// This enum's values correspond to special feature sets a plugin may provide.
+/// They are meant to be or'ed together to represent the total feature set of a plugin.
+enum Mumble_PluginFeature {
+ /// None of the below
+ FEATURE_NONE = 0,
+ /// The plugin provides positional data from a game
+ FEATURE_POSITIONAL = 1 << 0,
+ /// The plugin modifies the input/output audio itself
+ FEATURE_AUDIO = 1 << 1
+};
+
+/// This enum's values represent talking states a user can be in when using Mumble.
+enum Mumble_TalkingState {
+ INVALID=-1,
+ PASSIVE=0,
+ TALKING,
+ WHISPERING,
+ SHOUTING,
+ TALKING_MUTED
+};
+
+/// This enum's values represent transmission modes a user might have configured. Transmission mode
+/// in this context is referring to a method that determines when a user is speaking and thus when
+/// to transmit audio packets.
+enum Mumble_TransmissionMode {
+ TM_CONTINOUS,
+ TM_VOICE_ACTIVATION,
+ TM_PUSH_TO_TALK
+};
+
+/// This enum's values represent the error codes that are being used by the MumbleAPI.
+/// You can get a string-representation for each error code via the errorMessage function.
+enum Mumble_ErrorCode {
+ EC_INTERNAL_ERROR = -2,
+ EC_GENERIC_ERROR = -1,
+ EC_OK = 0,
+ EC_POINTER_NOT_FOUND,
+ EC_NO_ACTIVE_CONNECTION,
+ EC_USER_NOT_FOUND,
+ EC_CHANNEL_NOT_FOUND,
+ EC_CONNECTION_NOT_FOUND,
+ EC_UNKNOWN_TRANSMISSION_MODE,
+ EC_AUDIO_NOT_AVAILABLE,
+ EC_INVALID_SAMPLE,
+ EC_INVALID_PLUGIN_ID,
+ EC_INVALID_MUTE_TARGET,
+ EC_CONNECTION_UNSYNCHRONIZED,
+ EC_INVALID_API_VERSION,
+ EC_UNSYNCHRONIZED_BLOB,
+ EC_UNKNOWN_SETTINGS_KEY,
+ EC_WRONG_SETTINGS_TYPE,
+ EC_SETTING_WAS_REMOVED,
+ EC_DATA_TOO_BIG,
+ EC_DATA_ID_TOO_LONG,
+};
+
+/// This enum's values represent error codes specific to the framework of handling positional data
+/// gathering (needed for Mumble's positional audio feature).
+enum Mumble_PositionalDataErrorCode {
+ /// Positional data has been initialized properly
+ PDEC_OK = 0,
+ /// Positional data is temporarily unavailable (e.g. because the corresponding process isn't running) but might be
+ /// at another point in time.
+ PDEC_ERROR_TEMP,
+ /// Positional data is permanently unavailable (e.g. because the respective memory offsets are outdated).
+ PDEC_ERROR_PERM
+};
+
+/// This enum's values represent keys for specific settings inside Mumble.
+enum Mumble_SettingsKey {
+ MSK_INVALID = -1,
+ MSK_AUDIO_INPUT_VOICE_HOLD = 0,
+ MSK_AUDIO_INPUT_VAD_SILENCE_THRESHOLD = 1,
+ MSK_AUDIO_INPUT_VAD_SPEECH_THRESHOLD = 2,
+ MSK_AUDIO_OUTPUT_PA_MINIMUM_DISTANCE = 3,
+ MSK_AUDIO_OUTPUT_PA_MAXIMUM_DISTANCE = 4,
+ MSK_AUDIO_OUTPUT_PA_BLOOM = 5,
+ MSK_AUDIO_OUTPUT_PA_MINIMUM_VOLUME = 6,
+};
+
+/// This enum's values represent the key-codes Mumble's API uses to reference keys on the keyboard.
+enum Mumble_KeyCode {
+ KC_INVALID = -1,
+
+ // Non-printable characters first
+ KC_NULL = 0,
+ KC_END = 1,
+ KC_LEFT = 2,
+ KC_RIGHT = 4,
+ KC_UP = 5,
+ KC_DOWN = 6,
+ KC_DELETE = 7,
+ KC_BACKSPACE = 8,
+ KC_TAB = 9,
+ KC_ENTER = 10, // == '\n'
+ KC_ESCAPE = 27,
+ KC_PAGE_UP = 11,
+ KC_PAGE_DOWN = 12,
+ KC_SHIFT = 13,
+ KC_CONTROL = 14,
+ KC_META = 15,
+ KC_ALT = 16,
+ KC_ALT_GR = 17,
+ KC_CAPSLOCK = 18,
+ KC_NUMLOCK = 19,
+ KC_SUPER = 20, // == windows key
+ KC_HOME = 21, // == Pos1
+ KC_PRINT = 22,
+ KC_SCROLLLOCK = 23,
+
+ // Printable characters are assigned to their ASCII code
+ KC_SPACE = ' ',
+ KC_EXCLAMATION_MARK = '!',
+ KC_DOUBLE_QUOTE = '"',
+ KC_HASHTAG = '#',
+ KC_DOLLAR = '$',
+ KC_PERCENT = '%',
+ KC_AMPERSAND = '&',
+ KC_SINGLE_QUOTE = '\'',
+ KC_OPEN_PARENTHESIS = '(',
+ KC_CLOSE_PARENTHESIS = ')',
+ KC_ASTERISK = '*',
+ KC_PLUS = '+',
+ KC_COMMA = ',',
+ KC_MINUS = '-',
+ KC_PERIOD = '.',
+ KC_SLASH = '/',
+ KC_0 = '0',
+ KC_1 = '1',
+ KC_2 = '2',
+ KC_3 = '3',
+ KC_4 = '4',
+ KC_5 = '5',
+ KC_6 = '6',
+ KC_7 = '7',
+ KC_8 = '8',
+ KC_9 = '9',
+ KC_COLON = ':',
+ KC_SEMICOLON = ';',
+ KC_LESS_THAN = '<',
+ KC_EQUALS = '=',
+ KC_GREATER_THAN = '>',
+ KC_QUESTION_MARK = '?',
+ KC_AT_SYMBOL = '@',
+ KC_A = 'A',
+ KC_B = 'B',
+ KC_C = 'C',
+ KC_D = 'D',
+ KC_E = 'E',
+ KC_F = 'F',
+ KC_G = 'G',
+ KC_H = 'H',
+ KC_I = 'I',
+ KC_J = 'J',
+ KC_K = 'K',
+ KC_L = 'L',
+ KC_M = 'M',
+ KC_N = 'N',
+ KC_O = 'O',
+ KC_P = 'P',
+ KC_Q = 'Q',
+ KC_R = 'R',
+ KC_S = 'S',
+ KC_T = 'T',
+ KC_U = 'U',
+ KC_V = 'V',
+ KC_W = 'W',
+ KC_X = 'X',
+ KC_Y = 'Y',
+ KC_Z = 'Z',
+ // leave out lowercase letters (for now)
+ KC_OPEN_BRACKET = '[',
+ KC_BACKSLASH = '\\',
+ KC_CLOSE_BRACKET = ']',
+ KC_CIRCUMFLEX = '^',
+ KC_UNDERSCORE = '_',
+ KC_GRAVE_AKCENT = '`',
+ KC_OPEN_BRACE = '{',
+ KC_VERTICAL_BAR = '|',
+ KC_CLOSE_BRACE = '}',
+ KC_TILDE = '~',
+
+ // Some characters from the extended ASCII code
+ KC_DEGREE_SIGN = 176,
+
+
+
+ // F-keys
+ // Start at a value of 256 as extended ASCII codes range up to 255
+ KC_F1 = 256,
+ KC_F2 = 257,
+ KC_F3 = 258,
+ KC_F4 = 259,
+ KC_F5 = 260,
+ KC_F6 = 261,
+ KC_F7 = 262,
+ KC_F8 = 263,
+ KC_F9 = 264,
+ KC_F10 = 265,
+ KC_F11 = 266,
+ KC_F12 = 267,
+ KC_F13 = 268,
+ KC_F14 = 269,
+ KC_F15 = 270,
+ KC_F16 = 271,
+ KC_F17 = 272,
+ KC_F18 = 273,
+ KC_F19 = 274,
+};
+
+/// A struct for representing a version of the form major.minor.patch
+struct Version {
+ int32_t major;
+ int32_t minor;
+ int32_t patch;
+#ifdef __cplusplus
+ bool operator<(const Version& other) const {
+ if (this->major != other.major) {
+ return this->major < other.major;
+ }
+ if (this->minor != other.minor) {
+ return this->minor < other.minor;
+ }
+ // Major and Minor are equal
+ return this->patch < other.patch;
+ }
+
+ bool operator>(const Version& other) const {
+ if (this->major != other.major) {
+ return this->major > other.major;
+ }
+ if (this->minor != other.minor) {
+ return this->minor > other.minor;
+ }
+ // Major and Minor are equal
+ return this->patch > other.patch;
+ }
+
+ bool operator>=(const Version& other) const {
+ if (this->major != other.major) {
+ return this->major > other.major;
+ }
+ if (this->minor != other.minor) {
+ return this->minor > other.minor;
+ }
+ // Major and Minor are equal
+ return this->patch >= other.patch;
+ }
+
+ bool operator<=(const Version& other) const {
+ if (this->major != other.major) {
+ return this->major < other.major;
+ }
+ if (this->minor != other.minor) {
+ return this->minor < other.minor;
+ }
+ // Major and Minor are equal
+ return this->patch <= other.patch;
+ }
+
+ bool operator==(const Version& other) const {
+ return this->major == other.major && this->minor == other.minor && this->patch == other.patch;
+ }
+
+ bool operator!=(const Version& other) const {
+ return this->major != other.major || this->minor != other.minor || this->patch != other.patch;
+ }
+
+ operator std::string() const {
+ return std::string("v") + std::to_string(this->major) + std::string(".") + std::to_string(this->minor) + std::string(".") + std::to_string(this->patch);
+ }
+
+#ifdef QT_VERSION
+ operator QString() const {
+ return QString::fromLatin1("v%0.%1.%2").arg(this->major).arg(this->minor).arg(this->patch);
+ }
+#endif
+#endif
+};
+
+/// Obtains a String representation for the given numeric error code.
+/// Note that the exact String representation corresponding to an error code may change and is thus
+/// not part of the plugin API as such. This function acts merely as a convenience helper for printing
+/// errors in a meaningful way.
+///
+/// @param errorCode The error code to get the String representation for
+/// @returns The error message coresponding to the given error code. The message
+/// is encoded as a C-string and is static, meaning that it is safe to use the
+/// returned pointer in your code.
+inline const char* errorMessage(int16_t errorCode) {
+ switch (errorCode) {
+ case EC_GENERIC_ERROR:
+ return "Generic error";
+ case EC_OK:
+ return "Ok - this is not an error";
+ case EC_POINTER_NOT_FOUND:
+ return "Can't find the passed pointer";
+ case EC_NO_ACTIVE_CONNECTION:
+ return "There is currently no active connection to a server";
+ case EC_USER_NOT_FOUND:
+ return "Can't find the requested user";
+ case EC_CHANNEL_NOT_FOUND:
+ return "Can't find the requested channel";
+ case EC_CONNECTION_NOT_FOUND:
+ return "Can't identify the requested connection";
+ case EC_UNKNOWN_TRANSMISSION_MODE:
+ return "Unknown transmission mode encountered";
+ case EC_AUDIO_NOT_AVAILABLE:
+ return "There is currently no audio output available";
+ case EC_INVALID_SAMPLE:
+ return "Attempted to use invalid sample (can't play it)";
+ case EC_INVALID_PLUGIN_ID:
+ return "Used an invalid plugin ID";
+ case EC_INVALID_MUTE_TARGET:
+ return "Used an invalid mute-target";
+ case EC_CONNECTION_UNSYNCHRONIZED:
+ return "The requested server connection has not yet finished synchronizing";
+ case EC_INVALID_API_VERSION:
+ return "The used API version is invalid or not supported";
+ case EC_UNSYNCHRONIZED_BLOB:
+ return "The requested blob (content) has not yet been synchronized between the client and the server";
+ case EC_UNKNOWN_SETTINGS_KEY:
+ return "The used settings-key does not match any key known to Mumble";
+ case EC_WRONG_SETTINGS_TYPE:
+ return "The referenced setting has a different type than requested";
+ case EC_SETTING_WAS_REMOVED:
+ return "The referenced setting got removed from Mumble and is no longer used";
+ case EC_DATA_TOO_BIG:
+ return "The given data is too large (exceeds limit)";
+ case EC_DATA_ID_TOO_LONG:
+ return "The given data ID is too long (exceeds limit)";
+ default:
+ return "Unknown error code";
+ }
+}
+
+
+/// This struct is used to return Strings from a plugin to Mumble. It is needed in order to
+/// work around the limitation of std::string not being part of C (it holds important information
+/// about the String's lifetime management requirements).
+struct MumbleStringWrapper {
+ /// The pointer to the actual String data
+ const char *data;
+ /// The size of the pointed String data
+ size_t size;
+ /// Whether the wrapped String needs to be released
+ /// after its usage. Instances for which this would be
+ /// false: Static Strings, String literals
+ bool needsReleasing;
+};
+
+/// Typedef for the type of a talking state
+typedef enum Mumble_TalkingState mumble_talking_state_t;
+/// Typedef for the type of a transmission mode
+typedef enum Mumble_TransmissionMode mumble_transmission_mode_t;
+/// Typedef for the type of a version
+typedef struct Version mumble_version_t;
+/// Typedef for the type of a connection
+typedef int32_t mumble_connection_t;
+/// Typedef for the type of a user
+typedef uint32_t mumble_userid_t;
+/// Typedef for the type of a channel
+typedef int32_t mumble_channelid_t;
+/// Typedef for the type of an error (code)
+typedef enum Mumble_ErrorCode mumble_error_t;
+/// Typedef for the type of a plugin ID
+typedef uint32_t mumble_plugin_id_t;
+/// Typedef for the type of a key to a setting in Mumble
+typedef enum Mumble_SettingsKey mumble_settings_key_t;
+/// Typedef for the type of a key-code
+typedef enum Mumble_KeyCode mumble_keycode_t;
+
+#endif // MUMBLE_PLUGINCOMPONENT_H_
diff --git a/plugins/Process.cpp b/plugins/Process.cpp
index 8d222a266..6f7042a53 100644
--- a/plugins/Process.cpp
+++ b/plugins/Process.cpp
@@ -5,7 +5,7 @@
#include "Process.h"
-#include "mumble_plugin_utils.h"
+#include "mumble_positional_audio_utils.h"
#include <chrono>
diff --git a/plugins/ProcessWindows.cpp b/plugins/ProcessWindows.cpp
index 234cb6123..3dc3472a9 100644
--- a/plugins/ProcessWindows.cpp
+++ b/plugins/ProcessWindows.cpp
@@ -5,7 +5,7 @@
#include "ProcessWindows.h"
-#include "mumble_plugin_win32_internals.h"
+#include "mumble_positional_audio_win32_internals.h"
ProcessWindows::ProcessWindows(const procid_t id, const std::string &name) : Process(id, name) {
const auto mods = modules();
diff --git a/plugins/amongus/Game.cpp b/plugins/amongus/Game.cpp
index decf24576..aa384d331 100644
--- a/plugins/amongus/Game.cpp
+++ b/plugins/amongus/Game.cpp
@@ -5,7 +5,7 @@
#include "Game.h"
-#include "mumble_plugin_utils.h"
+#include "../mumble_positional_audio_utils.h"
Game::Game(const procid_t id, const std::string name) : m_ok(false), m_proc(id, name) {
if (!m_proc.isOk()) {
diff --git a/plugins/amongus/amongus.cpp b/plugins/amongus/amongus.cpp
index 40d9caa9c..c671e72ea 100644
--- a/plugins/amongus/amongus.cpp
+++ b/plugins/amongus/amongus.cpp
@@ -5,8 +5,10 @@
#include "Game.h"
-#include "mumble_plugin.h"
-#include "mumble_plugin_utils.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_utils.h"
#include <sstream>
diff --git a/plugins/aoc/aoc.cpp b/plugins/aoc/aoc.cpp
index 0d818b1fd..9eb0c11c5 100644
--- a/plugins/aoc/aoc.cpp
+++ b/plugins/aoc/aoc.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
using namespace std;
diff --git a/plugins/arma2/arma2.cpp b/plugins/arma2/arma2.cpp
index af103973c..b73ad5721 100644
--- a/plugins/arma2/arma2.cpp
+++ b/plugins/arma2/arma2.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
procptr_t posptr, frontptr, topptr;
diff --git a/plugins/bf1/bf1.cpp b/plugins/bf1/bf1.cpp
index 17c1cee9d..766316018 100644
--- a/plugins/bf1/bf1.cpp
+++ b/plugins/bf1/bf1.cpp
@@ -3,8 +3,11 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h" // Include standard plugin header.
-#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape".
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h" // Include standard positional audio header
+#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape".
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &context, std::wstring &identity) {
diff --git a/plugins/bf1942/bf1942.cpp b/plugins/bf1942/bf1942.cpp
index a35a29784..4d2cc00f8 100644
--- a/plugins/bf1942/bf1942.cpp
+++ b/plugins/bf1942/bf1942.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
procptr_t faceptr, topptr;
// BYTE *stateptr;
diff --git a/plugins/bf2/bf2.cpp b/plugins/bf2/bf2.cpp
index 4e744487c..a08442eae 100644
--- a/plugins/bf2/bf2.cpp
+++ b/plugins/bf2/bf2.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
using namespace std;
diff --git a/plugins/bf2142/bf2142.cpp b/plugins/bf2142/bf2142.cpp
index aa1b2e4ef..7f7bb91d3 100644
--- a/plugins/bf2142/bf2142.cpp
+++ b/plugins/bf2142/bf2142.cpp
@@ -3,8 +3,11 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h" // Include standard plugin header.
-#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape".
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h" // Include standard positional audio header.
+#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape".
// Variable to contain module's addresses
procptr_t RendDX9 = 0;
diff --git a/plugins/bf3/bf3.cpp b/plugins/bf3/bf3.cpp
index 812e49134..e19198768 100644
--- a/plugins/bf3/bf3.cpp
+++ b/plugins/bf3/bf3.cpp
@@ -36,7 +36,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
static bool ptr_chain_valid = false;
diff --git a/plugins/bf4/bf4.cpp b/plugins/bf4/bf4.cpp
index f06fb379e..287dd8dc6 100644
--- a/plugins/bf4/bf4.cpp
+++ b/plugins/bf4/bf4.cpp
@@ -3,8 +3,11 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h" // Include standard plugin header.
-#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape".
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h" // Include standard positional audio header.
+#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape".
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &context, std::wstring &identity) {
diff --git a/plugins/bf4_x86/bf4_x86.cpp b/plugins/bf4_x86/bf4_x86.cpp
index c4d49620f..af7400e00 100644
--- a/plugins/bf4_x86/bf4_x86.cpp
+++ b/plugins/bf4_x86/bf4_x86.cpp
@@ -3,8 +3,11 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h" // Include standard plugin header.
-#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape".
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h" // Include standard positional audio header.
+#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape".
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &context, std::wstring &identity) {
diff --git a/plugins/bfbc2/bfbc2.cpp b/plugins/bfbc2/bfbc2.cpp
index 312498e37..cd31f737a 100644
--- a/plugins/bfbc2/bfbc2.cpp
+++ b/plugins/bfbc2/bfbc2.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
bool is_steam = false;
diff --git a/plugins/bfheroes/bfheroes.cpp b/plugins/bfheroes/bfheroes.cpp
index fc7136737..f92ded261 100644
--- a/plugins/bfheroes/bfheroes.cpp
+++ b/plugins/bfheroes/bfheroes.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
procptr_t posptr, faceptr, topptr, stateptr;
diff --git a/plugins/blacklight/blacklight.cpp b/plugins/blacklight/blacklight.cpp
index ffc016f98..e94677761 100644
--- a/plugins/blacklight/blacklight.cpp
+++ b/plugins/blacklight/blacklight.cpp
@@ -34,7 +34,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
/*
Arrays of bytes to find addresses accessed by respective functions so we don't have to blindly search for addresses
diff --git a/plugins/borderlands/borderlands.cpp b/plugins/borderlands/borderlands.cpp
index 46aabf210..bca3f2ac6 100644
--- a/plugins/borderlands/borderlands.cpp
+++ b/plugins/borderlands/borderlands.cpp
@@ -34,7 +34,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
procptr_t posptr, frontptr, topptr, contextptraddress, stateaddress, loginaddress;
diff --git a/plugins/borderlands2/borderlands2.cpp b/plugins/borderlands2/borderlands2.cpp
index 2ef7b0a1f..4e58f0dfc 100644
--- a/plugins/borderlands2/borderlands2.cpp
+++ b/plugins/borderlands2/borderlands2.cpp
@@ -35,7 +35,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
#include <algorithm>
diff --git a/plugins/breach/breach.cpp b/plugins/breach/breach.cpp
index 605b16a94..16185714b 100644
--- a/plugins/breach/breach.cpp
+++ b/plugins/breach/breach.cpp
@@ -34,7 +34,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
procptr_t posptr, frontptr, topptr;
diff --git a/plugins/cod2/cod2.cpp b/plugins/cod2/cod2.cpp
index 1baef190b..05739facb 100644
--- a/plugins/cod2/cod2.cpp
+++ b/plugins/cod2/cod2.cpp
@@ -5,8 +5,10 @@
#include "ProcessWindows.h"
-#include "mumble_plugin.h"
-#include "mumble_plugin_utils.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_utils.h"
std::unique_ptr< ProcessWindows > process;
diff --git a/plugins/cod4/cod4.cpp b/plugins/cod4/cod4.cpp
index 05c504014..9a6155bdd 100644
--- a/plugins/cod4/cod4.cpp
+++ b/plugins/cod4/cod4.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
using namespace std;
diff --git a/plugins/cod5/cod5.cpp b/plugins/cod5/cod5.cpp
index cf5982383..e27cfa892 100644
--- a/plugins/cod5/cod5.cpp
+++ b/plugins/cod5/cod5.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &, std::wstring &) {
diff --git a/plugins/codmw2/codmw2.cpp b/plugins/codmw2/codmw2.cpp
index 44b4d9527..9daf9a84f 100644
--- a/plugins/codmw2/codmw2.cpp
+++ b/plugins/codmw2/codmw2.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &, std::wstring &) {
diff --git a/plugins/codmw2so/codmw2so.cpp b/plugins/codmw2so/codmw2so.cpp
index cac4484a7..25122bb62 100644
--- a/plugins/codmw2so/codmw2so.cpp
+++ b/plugins/codmw2so/codmw2so.cpp
@@ -35,7 +35,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &, std::wstring &) {
diff --git a/plugins/cs/cs.cpp b/plugins/cs/cs.cpp
index 2d8e8730e..2097dd8fe 100644
--- a/plugins/cs/cs.cpp
+++ b/plugins/cs/cs.cpp
@@ -35,7 +35,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
using namespace std;
diff --git a/plugins/dys/dys.cpp b/plugins/dys/dys.cpp
index 946731c3d..2fa0a9034 100644
--- a/plugins/dys/dys.cpp
+++ b/plugins/dys/dys.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
using namespace std;
diff --git a/plugins/etqw/etqw.cpp b/plugins/etqw/etqw.cpp
index 124848d2a..e5f1a2b57 100644
--- a/plugins/etqw/etqw.cpp
+++ b/plugins/etqw/etqw.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
using namespace std;
diff --git a/plugins/ffxiv/ffxiv.cpp b/plugins/ffxiv/ffxiv.cpp
index c1643a24c..f7742bcdc 100644
--- a/plugins/ffxiv/ffxiv.cpp
+++ b/plugins/ffxiv/ffxiv.cpp
@@ -3,8 +3,11 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h" // Include standard plugin header.
-#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape".
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h" // Include standard positional audio header.
+#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape".
#include <cmath>
diff --git a/plugins/gmod/gmod.cpp b/plugins/gmod/gmod.cpp
index fd2bb2153..d13b9ab88 100644
--- a/plugins/gmod/gmod.cpp
+++ b/plugins/gmod/gmod.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
using namespace std;
diff --git a/plugins/gtaiv/gtaiv.cpp b/plugins/gtaiv/gtaiv.cpp
index b57cb7d84..18db5d817 100644
--- a/plugins/gtaiv/gtaiv.cpp
+++ b/plugins/gtaiv/gtaiv.cpp
@@ -34,7 +34,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
static unsigned int playerid;
static procptr_t base_address;
diff --git a/plugins/gtasa/gtasa.cpp b/plugins/gtasa/gtasa.cpp
index cb166b5d4..293b2d115 100644
--- a/plugins/gtasa/gtasa.cpp
+++ b/plugins/gtasa/gtasa.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
struct Matrix {
float right[4];
diff --git a/plugins/gtav/gtav.cpp b/plugins/gtav/gtav.cpp
index a10e75888..d00237c76 100644
--- a/plugins/gtav/gtav.cpp
+++ b/plugins/gtav/gtav.cpp
@@ -3,8 +3,11 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h" // Include standard plugin header.
-#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape".
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h" // Include standard positional audio header.
+#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape".
#include <algorithm> // Include algorithm header for the game version detector
diff --git a/plugins/gw/gw.cpp b/plugins/gw/gw.cpp
index 567a5d14f..b8fbeab57 100644
--- a/plugins/gw/gw.cpp
+++ b/plugins/gw/gw.cpp
@@ -34,7 +34,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
/*
Arrays of bytes to find addresses accessed by respective functions so we don't have to blindly search for addresses
diff --git a/plugins/insurgency/insurgency.cpp b/plugins/insurgency/insurgency.cpp
index 89f4ce811..f7ce9d405 100644
--- a/plugins/insurgency/insurgency.cpp
+++ b/plugins/insurgency/insurgency.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
using namespace std;
diff --git a/plugins/jc2/jc2.cpp b/plugins/jc2/jc2.cpp
index 5d8c66b58..521da373e 100644
--- a/plugins/jc2/jc2.cpp
+++ b/plugins/jc2/jc2.cpp
@@ -35,7 +35,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
const unsigned int off_character_manager = 0xD8FB24;
const unsigned int off_local_player = 0x3570;
diff --git a/plugins/link/link-posix.cpp b/plugins/link/link-posix.cpp
index 43306e21b..c29e6de4b 100644
--- a/plugins/link/link-posix.cpp
+++ b/plugins/link/link-posix.cpp
@@ -3,7 +3,8 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
#include <stdint.h>
#include <stdio.h>
diff --git a/plugins/link/link.cpp b/plugins/link/link.cpp
index a899affd5..f98bed2e4 100644
--- a/plugins/link/link.cpp
+++ b/plugins/link/link.cpp
@@ -10,7 +10,8 @@
#include <windows.h>
#include <math.h>
-#include "../mumble_plugin.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
static std::wstring wsPluginName;
static std::wstring wsDescription;
diff --git a/plugins/lol/lol.cpp b/plugins/lol/lol.cpp
index 9746cc4a8..4f70ced4f 100644
--- a/plugins/lol/lol.cpp
+++ b/plugins/lol/lol.cpp
@@ -34,7 +34,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
/*
Arrays of bytes to find addresses accessed by respective functions so we don't have to blindly search for addresses
diff --git a/plugins/lotro/lotro.cpp b/plugins/lotro/lotro.cpp
index ec5c79cb1..f3562236f 100644
--- a/plugins/lotro/lotro.cpp
+++ b/plugins/lotro/lotro.cpp
@@ -34,7 +34,10 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &context, std::wstring &) {
diff --git a/plugins/mumble_plugin.h b/plugins/mumble_legacy_plugin.h
index bed4383aa..14c35cf40 100644
--- a/plugins/mumble_plugin.h
+++ b/plugins/mumble_legacy_plugin.h
@@ -1,10 +1,23 @@
-// Copyright 2005-2021 The Mumble Developers. All rights reserved.
+// Copyright 2021 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#ifndef MUMBLE_MUMBLE_PLUGIN_H_
-#define MUMBLE_MUMBLE_PLUGIN_H_
+// This header describes the deprecated "plugin" API from the times in which "plugins" could only be used
+// alongside the positional audio feature of Mumble.
+// By default translation units including this file will not compile due to a preprocessor error. This is intended
+// behaviour as you shouldn't be using this API any longer. Use the API from MumblePlugin.h instead.
+//
+// If for some reason you absolutely have to include this header file, you have to define the macro MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+// before including this header.
+#ifndef MUMBLE_LEGACY_PLUGIN_H_
+#define MUMBLE_LEGACY_PLUGIN_H_
+
+#ifndef MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+ #error "You are trying to use a deprecated plugin API. Use the new API from MumblePlugin.h instead. If you think you really need this deprecated one, see the instructions at the top of this file."
+#else
+ #undef MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#endif
#include <stdint.h>
diff --git a/plugins/mumble_plugin_linux.h b/plugins/mumble_positional_audio_linux.h
index 15a26f26e..6758e63b8 100644
--- a/plugins/mumble_plugin_linux.h
+++ b/plugins/mumble_positional_audio_linux.h
@@ -1,16 +1,16 @@
-// Copyright 2016-2021 The Mumble Developers. All rights reserved.
+// Copyright 2021 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#ifndef MUMBLE_PLUGIN_LINUX_H_
-#define MUMBLE_PLUGIN_LINUX_H_
+#ifndef MUMBLE_POSITIONAL_AUDIO_LINUX_H_
+#define MUMBLE_POSITIONAL_AUDIO_LINUX_H_
-#ifndef MUMBLE_PLUGIN_MAIN_H_
-# error "Include mumble_plugin_main.h instead of mumble_plugin_linux.h"
+#ifndef MUMBLE_POSITIONAL_AUDIO_MAIN_H_
+# error "Include mumble_positional_audio_main.h instead of mumble_positional_audio_linux.h"
#endif
-#include "mumble_plugin_utils.h"
+#include "mumble_positional_audio_utils.h"
#include <cstring>
#include <elf.h>
diff --git a/plugins/mumble_plugin_main.h b/plugins/mumble_positional_audio_main.h
index 1024c431b..c8966535f 100644
--- a/plugins/mumble_plugin_main.h
+++ b/plugins/mumble_positional_audio_main.h
@@ -1,4 +1,4 @@
-// Copyright 2019-2021 The Mumble Developers. All rights reserved.
+// Copyright 2021 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
@@ -11,15 +11,14 @@
// base address of a module (e.g. shared library), the latter reads memory at the
// specified address and stores it in a variable.
-#ifndef MUMBLE_PLUGIN_MAIN_H_
-#define MUMBLE_PLUGIN_MAIN_H_
+#ifndef MUMBLE_POSITIONAL_AUDIO_MAIN_H_
+#define MUMBLE_POSITIONAL_AUDIO_MAIN_H_
#if !defined(OS_WINDOWS) && !defined(OS_LINUX)
# error "Please define either OS_WINDOWS or OS_LINUX"
#endif
-#include "mumble_plugin.h"
-#include "mumble_plugin_win32_internals.h"
+#include "mumble_positional_audio_win32_internals.h"
#include <chrono>
#include <cstring>
@@ -228,10 +227,10 @@ static inline int8_t isWin32Process64Bit(const procptr_t &baseAddress) {
}
}
-#ifdef OS_WINDOWS
-# include "../mumble_plugin_win32.h"
+#ifdef WIN32
+# include "../mumble_positional_audio_win32.h"
#else
-# include "../mumble_plugin_linux.h"
+# include "../mumble_positional_audio_linux.h"
#endif
#endif
diff --git a/plugins/mumble_plugin_utils.h b/plugins/mumble_positional_audio_utils.h
index 355239d2c..ca6b5b4f0 100644
--- a/plugins/mumble_plugin_utils.h
+++ b/plugins/mumble_positional_audio_utils.h
@@ -1,10 +1,10 @@
-// Copyright 2016-2021 The Mumble Developers. All rights reserved.
+// Copyright 2021 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#ifndef MUMBLE_MUMBLE_PLUGIN_UTILS_H_
-#define MUMBLE_MUMBLE_PLUGIN_UTILS_H_
+#ifndef MUMBLE_POSITIONAL_AUDIO_UTILS_H_
+#define MUMBLE_POSITIONAL_AUDIO_UTILS_H_
#include <cmath>
#include <codecvt>
diff --git a/plugins/mumble_plugin_win32.h b/plugins/mumble_positional_audio_win32.h
index b2d530579..246002d01 100644
--- a/plugins/mumble_plugin_win32.h
+++ b/plugins/mumble_positional_audio_win32.h
@@ -1,13 +1,13 @@
-// Copyright 2010-2021 The Mumble Developers. All rights reserved.
+// Copyright 2021 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#ifndef MUMBLE_MUMBLE_PLUGIN_WIN32_H_
-#define MUMBLE_MUMBLE_PLUGIN_WIN32_H_
+#ifndef MUMBLE_POSITIONAL_AUDIO_WIN32_H_
+#define MUMBLE_POSITIONAL_AUDIO_WIN32_H_
-#ifndef MUMBLE_PLUGIN_MAIN_H_
-# error "Include mumble_plugin_main.h instead of mumble_plugin_win32.h"
+#ifndef MUMBLE_POSITIONAL_AUDIO_MAIN_H_
+# error "Include mumble_positional_audio_main.h instead of mumble_positional_audio_win32.h"
#endif
#include <stdio.h>
diff --git a/plugins/mumble_plugin_win32_internals.h b/plugins/mumble_positional_audio_win32_internals.h
index 81662589e..671a39dac 100644
--- a/plugins/mumble_plugin_win32_internals.h
+++ b/plugins/mumble_positional_audio_win32_internals.h
@@ -1,10 +1,10 @@
-// Copyright 2019-2021 The Mumble Developers. All rights reserved.
+// Copyright 2021 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#ifndef MUMBLE_MUMBLE_PLUGIN_WIN32_INTERNALS_H_
-#define MUMBLE_MUMBLE_PLUGIN_WIN32_INTERNALS_H_
+#ifndef MUMBLE_MUMBLE_POSITIONAL_AUDIO_WIN32_INTERNALS_H_
+#define MUMBLE_MUMBLE_POSITIONAL_AUDIO_WIN32_INTERNALS_H_
// These structures represent the header(s) of an NT image.
//
diff --git a/plugins/null_plugin.cpp b/plugins/null_plugin.cpp
index a41f30444..ed9d38cce 100644
--- a/plugins/null_plugin.cpp
+++ b/plugins/null_plugin.cpp
@@ -3,7 +3,8 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "mumble_plugin.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "mumble_legacy_plugin.h"
#ifndef NULL_DESC
# define NULL_DESC L"Retracted plugin"
diff --git a/plugins/ql/ql.cpp b/plugins/ql/ql.cpp
index 0905c2bd0..69c0441a3 100644
--- a/plugins/ql/ql.cpp
+++ b/plugins/ql/ql.cpp
@@ -3,8 +3,11 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h" // Include standard plugin header.
-#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape".
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h" // Include standard positional audio header.
+#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape".
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &context, std::wstring &identity) {
diff --git a/plugins/rl/rl.cpp b/plugins/rl/rl.cpp
index eabe7707c..5d8ee9f0a 100644
--- a/plugins/rl/rl.cpp
+++ b/plugins/rl/rl.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
#ifdef WIN32
// Memory offsets
diff --git a/plugins/se/se.cpp b/plugins/se/se.cpp
index f048ea3ad..15bcd759f 100644
--- a/plugins/se/se.cpp
+++ b/plugins/se/se.cpp
@@ -3,8 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "mumble_plugin.h"
-#include "mumble_plugin_utils.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_utils.h"
#ifdef OS_LINUX
# include "ProcessLinux.h"
diff --git a/plugins/sr/sr.cpp b/plugins/sr/sr.cpp
index 0482179d6..9ec80b8f7 100644
--- a/plugins/sr/sr.cpp
+++ b/plugins/sr/sr.cpp
@@ -35,7 +35,10 @@
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string & /*context*/, std::wstring & /*identity*/) {
diff --git a/plugins/testPlugin/CMakeLists.txt b/plugins/testPlugin/CMakeLists.txt
new file mode 100644
index 000000000..6dea29e32
--- /dev/null
+++ b/plugins/testPlugin/CMakeLists.txt
@@ -0,0 +1,9 @@
+# Copyright 2021 The Mumble Developers. All rights reserved.
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file at the root of the
+# Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+if(${CMAKE_BUILD_TYPE} MATCHES Debug)
+ message("Including TestPlugin in debug mode")
+ add_library(testPlugin SHARED "testPlugin.cpp")
+endif()
diff --git a/plugins/testPlugin/testPlugin.cpp b/plugins/testPlugin/testPlugin.cpp
new file mode 100644
index 000000000..e18822fed
--- /dev/null
+++ b/plugins/testPlugin/testPlugin.cpp
@@ -0,0 +1,480 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+// Include the definitions of the plugin functions
+// Not that this will also include ../PluginComponents.h
+#include "../MumblePlugin_v_1_0_x.h"
+#include "../MumbleAPI_v_1_0_x.h"
+
+#include <iostream>
+#include <cstring>
+
+// These are just some utility functions facilitating writing logs and the like
+// The actual implementation of the plugin is further down
+std::ostream& pLog() {
+ std::cout << "TestPlugin: ";
+ return std::cout;
+}
+
+template<typename T>
+void pluginLog(T log) {
+ pLog() << log << std::endl;
+}
+
+std::ostream& operator<<(std::ostream& stream, const mumble_version_t version) {
+ stream << "v" << version.major << "." << version.minor << "." << version.patch;
+ return stream;
+}
+
+
+//////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////
+//////////////////// PLUGIN IMPLEMENTATION ///////////////////
+//////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////////
+
+MumbleAPI_v_1_0_x mumAPI;
+mumble_connection_t activeConnection;
+mumble_plugin_id_t ownID;
+
+//////////////////////////////////////////////////////////////
+//////////////////// OBLIGATORY FUNCTIONS ////////////////////
+//////////////////////////////////////////////////////////////
+// All of the following function must be implemented in order for Mumble to load the plugin
+
+mumble_error_t mumble_init(uint32_t id) {
+ pluginLog("Initialized plugin");
+
+ ownID = id;
+
+ // Print the connection ID at initialization. If not connected to a server it should be -1.
+ pLog() << "Plugin ID is " << id << std::endl;
+
+ mumAPI.log(ownID, "Intitialized");
+
+ // Little showcase for how to retrieve a setting from Mumble
+ int64_t voiceHold;
+ mumble_error_t error = mumAPI.getMumbleSetting_int(ownID, MSK_AUDIO_INPUT_VOICE_HOLD, &voiceHold);
+ if (error == STATUS_OK) {
+ pLog() << "The voice hold is set to " << voiceHold << std::endl;
+ } else {
+ pluginLog("Failed to retrieve voice hold");
+ pLog() << errorMessage(error) << std::endl;
+ }
+
+ // STATUS_OK is a macro set to the appropriate status flag (ErrorCode)
+ // If you need to return any other status have a look at the ErrorCode enum
+ // inside PluginComponents.h and use one of its values
+ return STATUS_OK;
+}
+
+void mumble_shutdown() {
+ pluginLog("Shutdown plugin");
+
+ mumAPI.log(ownID, "Shutdown");
+}
+
+MumbleStringWrapper mumble_getName() {
+ static const char *name = "TestPlugin";
+
+ MumbleStringWrapper wrapper;
+ wrapper.data = name;
+ wrapper.size = strlen(name);
+ // It's a static String and therefore doesn't need releasing
+ wrapper.needsReleasing = false;
+
+ return wrapper;
+}
+
+mumble_version_t mumble_getAPIVersion() {
+ // MUMBLE_PLUGIN_API_VERSION will always contain the API version of the used header file (the one used to build
+ // this plugin against). Thus you should always return that here in order to no have to worry about it.
+ return MUMBLE_PLUGIN_API_VERSION;
+}
+
+void mumble_registerAPIFunctions(void *api) {
+ // In this function the plugin is presented with a struct of function pointers that can be used
+ // to interact with Mumble. Thus you should store it somewhere safe for later usage.
+
+ // The pointer has to be cast to the respective API struct. You always have to cast to the same API version
+ // as this plugin itself is using. Thus if this plugin is compiled using the API version 1.0.x (where x is an arbitrary version)
+ // the pointer has to be cast to MumbleAPI_v_1_0_x (where x is a literal "x").
+ // Furthermore the struct HAS TO BE COPIED!!! Storing the pointer is not an option as it will become invalid quickly!
+
+ // **If** you are using the same API version that is specified in the included header file (as you should), you
+ // can simply use the MUMBLE_API_CAST to cast the pointer to the correct type and automatically dereferencing it.
+ mumAPI = MUMBLE_API_CAST(api);
+
+ pluginLog("Registered Mumble's API functions");
+}
+
+void mumble_releaseResource(const void *pointer) {
+ std::cerr << "[ERROR]: Unexpected call to mumble_releaseResources" << std::endl;
+ std::terminate();
+ // This plugin doesn't use resources that are explicitly allocated (only static Strings are used). Therefore
+ // we don't have to implement this function.
+ //
+ // If you allocated resources using malloc(), you're implementation for releasing that would be
+ // free(const_cast<void *>(pointer));
+ //
+ // If however you allocated a resource using the new operator (C++ only), you have figure out the pointer's
+ // original type and then invoke
+ // delete static_cast<ActualType *>(pointer);
+
+ // Mark as unused
+ (void) pointer;
+}
+
+
+//////////////////////////////////////////////////////////////
+///////////////////// OPTIONAL FUNCTIONS /////////////////////
+//////////////////////////////////////////////////////////////
+// The implementation of below functions is optional. If you don't need them, don't include them in your
+// plugin
+
+void mumble_setMumbleInfo(mumble_version_t mumbleVersion, mumble_version_t mumbleAPIVersion, mumble_version_t minimumExpectedAPIVersion) {
+ // this function will always be the first one to be called. Even before init()
+ // In here you can get info about the Mumble version this plugin is about to run in.
+ pLog() << "Mumble version: " << mumbleVersion << "; Mumble API-Version: " << mumbleAPIVersion << "; Minimal expected API-Version: "
+ << minimumExpectedAPIVersion << std::endl;
+}
+
+mumble_version_t mumble_getVersion() {
+ // Mumble uses semantic versioning (see https://semver.org/)
+ // { major, minor, patch }
+ return { 1, 0, 0 };
+}
+
+MumbleStringWrapper mumble_getAuthor() {
+ static const char *author = "MumbleDevelopers";
+
+ MumbleStringWrapper wrapper;
+ wrapper.data = author;
+ wrapper.size = strlen(author);
+ // It's a static String and therefore doesn't need releasing
+ wrapper.needsReleasing = false;
+
+ return wrapper;
+}
+
+MumbleStringWrapper mumble_getDescription() {
+ static const char *description =
+ "This plugin is merely a reference implementation without any real functionality. It shouldn't be included in the release build of Mumble.";
+
+ MumbleStringWrapper wrapper;
+ wrapper.data = description;
+ wrapper.size = strlen(description);
+ // It's a static String and therefore doesn't need releasing
+ wrapper.needsReleasing = false;
+
+ return wrapper;
+}
+
+uint32_t mumble_getFeatures() {
+ // Tells Mumble whether this plugin delivers some known common functionality. See the PluginFeature enum in
+ // PluginComponents.h for what is available.
+ // If you want your plugin to deliver positional data, you'll want to return FEATURE_POSITIONAL
+ return FEATURE_NONE;
+}
+
+uint32_t mumble_deactivateFeatures(uint32_t features) {
+ pLog() << "Asked to deactivate feature set " << features << std::endl;
+
+ // All features that can't be deactivated should be returned
+ return features;
+}
+
+uint8_t mumble_initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) {
+ std::ostream& stream = pLog() << "Got " << programCount << " programs to init positional data.";
+
+ if (programCount > 0) {
+ stream << " The first name is " << programNames[0] << " and has PID " << programPIDs[0];
+ }
+
+ stream << std::endl;
+
+ // As this plugin doesn't provide PD, we return PDEC_ERROR_PERM to indicate that even in the future we won't do so
+ // If your plugin is indeed delivering positional data but is only temporarily unable to do so, return PDEC_ERROR_TEMP.
+ // and if you deliver PD and succeeded initializing return PDEC_OK.
+ return PDEC_ERROR_PERM;
+}
+
+#define SET_TO_ZERO(name) name[0] = 0.0f; name[1] = 0.0f; name[2] = 0.0f
+bool mumble_fetchPositionalData(float *avatarPos, float *avatarDir, float *avatarAxis, float *cameraPos, float *cameraDir,
+ float *cameraAxis, const char **context, const char **identity) {
+ pluginLog("Has been asked to deliver positional data");
+
+ // If unable to provide positional data, this function should return false and reset all given values to 0 / empty Strings
+ SET_TO_ZERO(avatarPos);
+ SET_TO_ZERO(avatarDir);
+ SET_TO_ZERO(avatarAxis);
+ SET_TO_ZERO(cameraPos);
+ SET_TO_ZERO(cameraDir);
+ SET_TO_ZERO(cameraAxis);
+ *context = "";
+ *identity = "";
+
+ // This function returns whether it can continue to deliver positional data
+ return false;
+}
+#undef SET_TO_ZERO
+
+void mumble_shutdownPositionalData() {
+ pluginLog("Shutting down positional data");
+}
+
+void mumble_onServerConnected(mumble_connection_t connection) {
+ activeConnection = connection;
+
+ pLog() << "Established server-connection with ID " << connection << std::endl;
+
+ // Use API function that'll block
+ mumAPI.log(ownID, "Connected to a server");
+}
+
+void mumble_onServerDisconnected(mumble_connection_t connection) {
+ activeConnection = -1;
+
+ const char *serverHash;
+ if (mumAPI.getServerHash(ownID, connection, &serverHash) == STATUS_OK) {
+ pLog() << "Disconnected from server-connection with ID " << connection << "(hash: " << serverHash << ")" << std::endl;
+
+ mumAPI.freeMemory(ownID, serverHash);
+ } else {
+ pluginLog("[ERROR]: mumble_onServerDisconnected - Unable to fetch server-hash");
+ }
+}
+
+void mumble_onServerSynchronized(mumble_connection_t connection) {
+ // The client has finished synchronizing with the server. Thus we can now obtain a list of all users on this server
+ const char *serverHash;
+ if (mumAPI.getServerHash(ownID, connection, &serverHash) == STATUS_OK) {
+ pLog() << "Server has finished synchronizing (ServerConnection: " << connection << "; hash: " << serverHash << ")" << std::endl ;
+
+ mumAPI.freeMemory(ownID, serverHash);
+ } else {
+ pluginLog("[ERROR]: mumble_onServerSynchronized - Unable to fetch server-hash");
+ }
+
+ size_t userCount;
+ mumble_userid_t *userIDs;
+
+ if (mumAPI.getAllUsers(ownID, activeConnection, &userIDs, &userCount) != STATUS_OK) {
+ pluginLog("[ERROR]: Can't obtain user list");
+ return;
+ }
+
+ mumble_userid_t localUserID;
+ if (mumAPI.getLocalUserID(ownID, connection, &localUserID) != STATUS_OK) {
+ pluginLog("[ERROR]: Can't obtain ID of local user");
+ return;
+ }
+
+ pLog() << "There are " << userCount << " users on this server. Their names are:" << std::endl;
+
+ for(size_t i=0; i<userCount; i++) {
+ const char *userName;
+ if (mumAPI.getUserName(ownID, connection, userIDs[i], &userName) != STATUS_OK) {
+ pLog() << "<Unable to fetch user name>" << std::endl;
+ continue;
+ }
+
+ const char *userHash;
+ if (mumAPI.getUserHash(ownID, connection, userIDs[i], &userHash) != STATUS_OK) {
+ pluginLog("<Unable to get user-hash>");
+ }
+
+ pLog() << "\t" << userName << " (" << userHash << ")" << std::endl;
+
+ // Mute the user "MuteMe" if this is not the name of the local user (in which case it'd fail)
+ if (userIDs[i] != localUserID && std::strcmp(userName, "MuteMe") == 0) {
+ if (mumAPI.requestLocalMute(ownID, connection, userIDs[i], true) != STATUS_OK) {
+ pluginLog("[ERROR]: Failed at muting user \"MuteMe\"!");
+ }
+ }
+
+ mumAPI.freeMemory(ownID, userName);
+ mumAPI.freeMemory(ownID, userHash);
+ }
+
+ mumAPI.freeMemory(ownID, userIDs);
+
+ size_t channelCount;
+ mumble_channelid_t *channelIDs;
+
+ if (mumAPI.getAllChannels(ownID, activeConnection, &channelIDs, &channelCount) != STATUS_OK) {
+ pluginLog("[ERROR]: Failed to fetch channel list!");
+ return;
+ }
+
+ pLog() << "There are " << channelCount << " channels on this server" << std::endl;
+
+ mumAPI.freeMemory(ownID, channelIDs);
+
+ mumble_userid_t localUser;
+ if (mumAPI.getLocalUserID(ownID, activeConnection, &localUser) != STATUS_OK) {
+ pluginLog("Failed to retrieve local user ID");
+ return;
+ }
+
+ if (mumAPI.sendData(ownID, activeConnection, &localUser, 1, reinterpret_cast<const uint8_t *>("Just a test"), 12, "testMsg") == STATUS_OK) {
+ pluginLog("Successfully sent plugin message");
+
+ // Try break the rate-limiter for plugin messages
+ for (int i = 0; i < 40; i++) {
+ std::string data = "Rate-limit message #" + std::to_string(i);
+
+ mumAPI.sendData(ownID, activeConnection, &localUser, 1, reinterpret_cast<const uint8_t * >(data.c_str()), data.size(), "testMsg");
+ }
+ } else {
+ pluginLog("Failed at sending message");
+ }
+
+ if (mumAPI.requestSetLocalUserComment(ownID, connection, "This user has the TestPlugin enabled - <b>hand over a cookie!</b>") != STATUS_OK) {
+ pluginLog("Failed at setting the local user's comment");
+ }
+}
+
+void mumble_onChannelEntered(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t previousChannelID, mumble_channelid_t newChannelID) {
+ std::ostream& stream = pLog() << "User with ID " << userID << " entered channel with ID " << newChannelID << ".";
+
+ // negative ID means that there was no previous channel (e.g. because the user just connected)
+ if (previousChannelID >= 0) {
+ stream << " Came from channel with ID " << previousChannelID << ".";
+ }
+
+ stream << " (ServerConnection: " << connection << ")" << std::endl;
+}
+
+void mumble_onChannelExited(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t channelID) {
+ pLog() << "User with ID " << userID << " has left channel with ID " << channelID << ". (ServerConnection: " << connection << ")" << std::endl;
+}
+
+void mumble_onUserTalkingStateChanged(mumble_connection_t connection, mumble_userid_t userID, mumble_talking_state_t talkingState) {
+ std::ostream& stream = pLog() << "User with ID " << userID << " changed his talking state to ";
+
+ // The possible values are contained in the TalkingState enum inside PluginComponent.h
+ switch(talkingState) {
+ case INVALID:
+ stream << "Invalid";
+ break;
+ case PASSIVE:
+ stream << "Passive";
+ break;
+ case TALKING:
+ stream << "Talking";
+ break;
+ case WHISPERING:
+ stream << "Whispering";
+ break;
+ case SHOUTING:
+ stream << "Shouting";
+ break;
+ default:
+ stream << "Unknown (" << talkingState << ")";
+ }
+
+ stream << ". (ServerConnection: " << connection << ")" << std::endl;
+}
+
+bool mumble_onAudioInput(short *inputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech) {
+ // pLog() << "Audio input with " << channelCount << " channels and " << sampleCount << " samples per channel encountered. IsSpeech: "
+ // << isSpeech << " Sample rate is " << sampleRate << "Hz" << std::endl;
+
+ // mark variables as unused
+ (void) inputPCM;
+ (void) sampleCount;
+ (void) channelCount;
+ (void) sampleRate;
+ (void) isSpeech;
+
+ // This function returns whether it has modified the audio stream
+ return false;
+}
+
+bool mumble_onAudioSourceFetched(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech, mumble_userid_t userID) {
+ std::ostream& stream = pLog() << "Audio output source with " << channelCount << " channels and " << sampleCount << " samples per channel "
+ << "(" << sampleRate << " Hz) fetched.";
+
+ if (isSpeech) {
+ stream << " The output is speech from user with ID " << userID << ".";
+ }
+
+ stream << std::endl;
+
+ // Mark ouputPCM as unused
+ (void) outputPCM;
+
+ // This function returns whether it has modified the audio stream
+ return false;
+}
+
+bool mumble_onAudioOutputAboutToPlay(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate) {
+ // pLog() << "The resulting audio output has " << channelCount << " channels with " << sampleCount << " samples per channel ("
+ // sampleRate << " Hz)" << std::endl;
+
+ // mark variables as unused
+ (void) outputPCM;
+ (void) sampleCount;
+ (void) channelCount;
+ (void) sampleRate;
+
+ // This function returns whether it has modified the audio stream
+ return false;
+}
+
+bool mumble_onReceiveData(mumble_connection_t connection, mumble_userid_t sender, const uint8_t *data, size_t dataLength, const char *dataID) {
+ pLog() << "Received data with ID \"" << dataID << "\" from user with ID " << sender << ". Its length is " << dataLength
+ << ". (ServerConnection:" << connection << ")" << std::endl;
+
+ if (std::strcmp(dataID, "testMsg") == 0) {
+ // We know that data is only a normal C-encoded String, so the reinterpret_cast is safe
+ pLog() << "The received data: " << reinterpret_cast<const char *>(data) << std::endl;
+ }
+
+ // This function returns whether it has processed the data (preventing further plugins from seeing it)
+ return false;
+}
+
+void mumble_onUserAdded(mumble_connection_t connection, mumble_userid_t userID) {
+ pLog() << "Added user with ID " << userID << " (ServerConnection: " << connection << ")" << std::endl;
+}
+
+void mumble_onUserRemoved(mumble_connection_t connection, mumble_userid_t userID) {
+ pLog() << "Removed user with ID " << userID << " (ServerConnection: " << connection << ")" << std::endl;
+}
+
+void mumble_onChannelAdded(mumble_connection_t connection, mumble_channelid_t channelID) {
+ pLog() << "Added channel with ID " << channelID << " (ServerConnection: " << connection << ")" << std::endl;
+}
+
+void mumble_onChannelRemoved(mumble_connection_t connection, mumble_channelid_t channelID) {
+ pLog() << "Removed channel with ID " << channelID << " (ServerConnection: " << connection << ")" << std::endl;
+}
+
+void mumble_onChannelRenamed(mumble_connection_t connection, mumble_channelid_t channelID) {
+ pLog() << "Renamed channel with ID " << channelID << " (ServerConnection: " << connection << ")" << std::endl;
+}
+
+void mumble_onKeyEvent(uint32_t keyCode, bool wasPress) {
+ pLog() << "Encountered key " << (wasPress ? "press" : "release") << " of key with code " << keyCode << std::endl;
+}
+
+bool mumble_hasUpdate() {
+ // This plugin never has an update
+ return false;
+}
+
+MumbleStringWrapper mumble_getUpdateDownloadURL() {
+ static const char *url = "https://i.dont.exist/testplugin.zip";
+
+ MumbleStringWrapper wrapper;
+ wrapper.data = url;
+ wrapper.size = strlen(url);
+ // It's a static String and therefore doesn't need releasing
+ wrapper.needsReleasing = false;
+
+ return wrapper;
+}
diff --git a/plugins/ut2004/ut2004.cpp b/plugins/ut2004/ut2004.cpp
index 07a490fe1..1da0776ed 100644
--- a/plugins/ut2004/ut2004.cpp
+++ b/plugins/ut2004/ut2004.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
using namespace std;
diff --git a/plugins/ut3/ut3.cpp b/plugins/ut3/ut3.cpp
index bcf6d50c2..981ef29a3 100644
--- a/plugins/ut3/ut3.cpp
+++ b/plugins/ut3/ut3.cpp
@@ -3,7 +3,10 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
using namespace std;
diff --git a/plugins/ut99/ut99.cpp b/plugins/ut99/ut99.cpp
index 8831452ba..7ee98151a 100644
--- a/plugins/ut99/ut99.cpp
+++ b/plugins/ut99/ut99.cpp
@@ -34,8 +34,11 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-#include "../mumble_plugin_main.h"
-#include "../mumble_plugin_utils.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
+#include "../mumble_positional_audio_utils.h"
procptr_t posptr;
procptr_t frtptr;
diff --git a/plugins/wolfet/wolfet.cpp b/plugins/wolfet/wolfet.cpp
index a8827ae69..2dda0e743 100644
--- a/plugins/wolfet/wolfet.cpp
+++ b/plugins/wolfet/wolfet.cpp
@@ -47,7 +47,10 @@
Increasing when turning left.
*/
-#include "../mumble_plugin_main.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h"
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &context, std::wstring &) {
diff --git a/plugins/wow/wow.cpp b/plugins/wow/wow.cpp
index 5a4ffed6d..e64fe9b89 100644
--- a/plugins/wow/wow.cpp
+++ b/plugins/wow/wow.cpp
@@ -3,8 +3,11 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h" // Include standard plugin header.
-#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape".
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h" // Include standard positional audio header.
+#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape".
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &context, std::wstring &identity) {
diff --git a/plugins/wow_x64/wow_x64.cpp b/plugins/wow_x64/wow_x64.cpp
index f6ea550ad..50b04f6ad 100644
--- a/plugins/wow_x64/wow_x64.cpp
+++ b/plugins/wow_x64/wow_x64.cpp
@@ -3,8 +3,11 @@
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-#include "../mumble_plugin_main.h" // Include standard plugin header.
-#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape".
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../mumble_legacy_plugin.h"
+
+#include "../mumble_positional_audio_main.h" // Include standard positional audio header.
+#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape".
static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front,
float *camera_top, std::string &context, std::wstring &identity) {
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 7affde98e..747473b70 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -62,6 +62,7 @@ set(SHARED_SOURCES
"PasswordGenerator.cpp"
"PlatformCheck.cpp"
"QtUtils.cpp"
+ "ProcessResolver.cpp"
"SelfSignedCertificate.cpp"
"ServerAddress.cpp"
"ServerResolver.cpp"
@@ -96,6 +97,7 @@ set(SHARED_HEADERS
"OSInfo.h"
"PasswordGenerator.h"
"PlatformCheck.h"
+ "ProcessResolver.h"
"SelfSignedCertificate.h"
"ServerAddress.h"
"ServerResolver.h"
diff --git a/src/Channel.cpp b/src/Channel.cpp
index 749fa0865..2462b2edb 100644
--- a/src/Channel.cpp
+++ b/src/Channel.cpp
@@ -11,6 +11,9 @@
#include <QtCore/QStack>
#ifdef MUMBLE
+# include "PluginManager.h"
+# include "Global.h"
+
QHash< int, Channel * > Channel::c_qhChannels;
QReadWriteLock Channel::c_qrwlChannels;
#endif
@@ -66,6 +69,12 @@ Channel *Channel::add(int id, const QString &name) {
Channel *c = new Channel(id, name, nullptr);
c_qhChannels.insert(id, c);
+
+ // We have to use a direct connection here in order to make sure that the user object that gets passed to the callback
+ // does not get invalidated or deleted while the callback is running.
+ QObject::connect(c, &Channel::channelEntered, Global::get().pluginManager, &PluginManager::on_channelEntered, Qt::DirectConnection);
+ QObject::connect(c, &Channel::channelExited, Global::get().pluginManager, &PluginManager::on_channelExited, Qt::DirectConnection);
+
return c;
}
@@ -159,14 +168,20 @@ void Channel::removeChannel(Channel *c) {
}
void Channel::addUser(User *p) {
- if (p->cChannel)
- p->cChannel->removeUser(p);
+ Channel *prevChannel = p->cChannel;
+
+ if (prevChannel)
+ prevChannel->removeUser(p);
p->cChannel = this;
qlUsers << p;
+
+ emit channelEntered(this, prevChannel, p);
}
void Channel::removeUser(User *p) {
qlUsers.removeAll(p);
+
+ emit channelExited(this, p);
}
Channel::operator QString() const {
diff --git a/src/Channel.h b/src/Channel.h
index 578e83919..95e6bf972 100644
--- a/src/Channel.h
+++ b/src/Channel.h
@@ -99,6 +99,20 @@ public:
QSet< Channel * > allChildren();
operator QString() const;
+
+ signals:
+ /// Signal emitted whenever a user enters a channel.
+ ///
+ /// @param newChannel A pointer to the Channel the user has just entered
+ /// @param prevChannel A pointer to the Channel the user is coming from or nullptr if
+ /// there is no such channel.
+ /// @param user A pointer to the User that has triggered this signal
+ void channelEntered(const Channel *newChannel, const Channel *prevChannel, const User *user);
+ /// Signal emitted whenever a user leaves a channel.
+ ///
+ /// @param channel A pointer to the Channel the user has left
+ /// @param user A pointer to the User that has triggered this signal
+ void channelExited(const Channel *channel, const User *user);
};
#endif
diff --git a/src/Message.h b/src/Message.h
index 72ed4c00d..32c8d5f5f 100644
--- a/src/Message.h
+++ b/src/Message.h
@@ -41,7 +41,8 @@
MUMBLE_MH_MSG(UserStats) \
MUMBLE_MH_MSG(RequestBlob) \
MUMBLE_MH_MSG(ServerConfig) \
- MUMBLE_MH_MSG(SuggestConfig)
+ MUMBLE_MH_MSG(SuggestConfig) \
+ MUMBLE_MH_MSG(PluginDataTransmission)
class MessageHandler {
public:
diff --git a/src/Mumble.proto b/src/Mumble.proto
index 3075c6928..5132e7011 100644
--- a/src/Mumble.proto
+++ b/src/Mumble.proto
@@ -584,3 +584,16 @@ message SuggestConfig {
// True if the administrator suggests push to talk to be used on this server.
optional bool push_to_talk = 3;
}
+
+// Used to send plugin messages between clients
+message PluginDataTransmission {
+ // The session ID of the client this message was sent from
+ optional uint32 senderSession = 1;
+ // The session IDs of the clients that should receive this message
+ repeated uint32 receiverSessions = 2 [packed = true];
+ // The data that is sent
+ optional bytes data = 3;
+ // The ID of the sent data. This will be used by plugins to check whether they will
+ // process it or not
+ optional string dataID = 4;
+}
diff --git a/src/MumbleConstants.h b/src/MumbleConstants.h
new file mode 100644
index 000000000..df4fe49e5
--- /dev/null
+++ b/src/MumbleConstants.h
@@ -0,0 +1,20 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLECONSTANTS_H_
+#define MUMBLE_MUMBLECONSTANTS_H_
+
+namespace Mumble {
+namespace Plugins {
+ namespace PluginMessage {
+
+ constexpr int MAX_DATA_LENGTH = 1000;
+ constexpr int MAX_DATA_ID_LENGTH = 100;
+
+ }; // namespace PluginMessage
+}; // namespace Plugins
+}; // namespace Mumble
+
+#endif // MUMBLE_MUMBLECONSTANTS_H_
diff --git a/src/ProcessResolver.cpp b/src/ProcessResolver.cpp
new file mode 100644
index 000000000..39011d4ba
--- /dev/null
+++ b/src/ProcessResolver.cpp
@@ -0,0 +1,307 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "ProcessResolver.h"
+#include <cstring>
+
+ProcessResolver::ProcessResolver(bool resolveImmediately)
+ : m_processNames(),
+ m_processPIDs() {
+ if (resolveImmediately) {
+ resolve();
+ }
+}
+
+ProcessResolver::~ProcessResolver() {
+ freeAndClearData();
+}
+
+void ProcessResolver::freeAndClearData() {
+ // delete all names
+ foreach(const char *currentName, m_processNames) {
+ delete currentName;
+ }
+
+ m_processNames.clear();
+ m_processPIDs.clear();
+}
+
+const QVector<const char*>& ProcessResolver::getProcessNames() const {
+ return m_processNames;
+}
+
+const QVector<uint64_t>& ProcessResolver::getProcessPIDs() const {
+ return m_processPIDs;
+}
+
+void ProcessResolver::resolve() {
+ // first clear the current lists
+ freeAndClearData();
+
+ doResolve();
+}
+
+size_t ProcessResolver::amountOfProcesses() const {
+ return m_processPIDs.size();
+}
+
+
+/// Helper function to add a name stored as a stack-variable to the given vector
+///
+/// @param stackName The pointer to the stack-variable
+/// @param destVec The destination vector to add the pointer to
+void addName(const char *stackName, QVector<const char*>& destVec) {
+ // We can't store the pointer of a stack-variable (will be invalid as soon as we exit scope)
+ // so we'll have to allocate memory on the heap and copy the name there.
+ size_t nameLength = std::strlen(stackName) + 1; // +1 for terminating NULL-byte
+ char *name = new char[nameLength];
+
+ std::strcpy(name, stackName);
+
+ destVec.append(name);
+}
+
+// The implementation of the doResolve-function is platfrom-dependent
+// The different implementations are heavily inspired by the ones given at https://github.com/davidebeatrici/list-processes
+#ifdef Q_OS_WIN
+ // Implementation for Windows
+ #ifndef UNICODE
+ #define UNICODE
+ #endif
+
+ #ifndef WIN32_LEAN_AND_MEAN
+ #define WIN32_LEAN_AND_MEAN
+ #endif
+
+ #include <windows.h>
+ #include <tlhelp32.h>
+ #include <limits>
+
+ bool utf16ToUtf8(const wchar_t *source, const int size, char *destination) {
+ if (!WideCharToMultiByte(CP_UTF8, 0, source, -1, destination, size, NULL, NULL)) {
+#ifndef QT_NO_DEBUG
+ qCritical("ProcessResolver: WideCharToMultiByte() failed with error %d\n", GetLastError());
+#endif
+ return false;
+ }
+
+ return true;
+ }
+
+ void ProcessResolver::doResolve() {
+ HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+ if (hSnap == INVALID_HANDLE_VALUE) {
+#ifndef QT_NO_DEBUG
+ qCritical("ProcessResolver: CreateToolhelp32Snapshot() failed with error %d", GetLastError());
+#endif
+ return;
+ }
+
+ PROCESSENTRY32 pe;
+ pe.dwSize = sizeof(pe);
+
+ BOOL ok = Process32First(hSnap, &pe);
+ if (!ok) {
+#ifndef QT_NO_DEBUG
+ qCritical("ProcessResolver: Process32First() failed with error %d\n", GetLastError());
+#endif
+ return;
+ }
+
+ char name[MAX_PATH];
+
+ while (ok) {
+ if (utf16ToUtf8(pe.szExeFile, sizeof(name), name)) {
+ // Store name
+ addName(name, m_processNames);
+
+ // Store corresponding PID
+ m_processPIDs.append(pe.th32ProcessID);
+ }
+#ifndef QT_NO_DEBUG
+ else {
+ qWarning("ProcessResolver: utf16ToUtf8() failed, skipping entry...");
+ }
+#endif
+
+ ok = Process32Next(hSnap, &pe);
+ }
+
+ CloseHandle(hSnap);
+ }
+#elif defined(Q_OS_LINUX)
+ // Implementation for Linux
+ #include <QtCore/QFile>
+ #include <QtCore/QDir>
+ #include <QtCore/QStringList>
+ #include <QtCore/QFileInfo>
+ #include <QtCore/QByteArray>
+ #include <QtCore/QString>
+
+
+ static constexpr const char *PROC_DIR = "/proc/";
+
+ void ProcessResolver::doResolve() {
+ QDir procDir(QString::fromLatin1(PROC_DIR));
+ QStringList entries = procDir.entryList();
+
+ bool ok;
+
+ foreach(const QString& currentEntry, entries) {
+ uint64_t pid = static_cast<unsigned long long int>(currentEntry.toLongLong(&ok, 10));
+
+ if (!ok) {
+ continue;
+ }
+
+ QString exe = QFile::symLinkTarget(QString::fromLatin1(PROC_DIR) + currentEntry + QString::fromLatin1("/exe"));
+ QFileInfo fi(exe);
+ QString firstPart = fi.baseName();
+ QString completeSuffix = fi.completeSuffix();
+ QString baseName;
+ if (completeSuffix.isEmpty()) {
+ baseName = firstPart;
+ } else {
+ baseName = firstPart + QLatin1String(".") + completeSuffix;
+ }
+
+ if (baseName == QLatin1String("wine-preloader") || baseName == QLatin1String("wine64-preloader")) {
+ QFile f(QString::fromLatin1(PROC_DIR) + currentEntry + QString::fromLatin1("/cmdline"));
+ if (f.open(QIODevice::ReadOnly)) {
+ QByteArray cmdline = f.readAll();
+ f.close();
+
+ int nul = cmdline.indexOf('\0');
+ if (nul != -1) {
+ cmdline.truncate(nul);
+ }
+
+ QString exe = QString::fromUtf8(cmdline);
+ if (exe.contains(QLatin1String("\\"))) {
+ int lastBackslash = exe.lastIndexOf(QLatin1String("\\"));
+ if (exe.count() > lastBackslash + 1) {
+ baseName = exe.mid(lastBackslash + 1);
+ }
+ }
+ }
+ }
+
+ if (!baseName.isEmpty()) {
+ // add name
+ addName(baseName.toUtf8().data(), m_processNames);
+
+ // add corresponding PID
+ m_processPIDs.append(pid);
+ }
+ }
+ }
+#elif defined(Q_OS_MACOS)
+ // Implementation for MacOS
+ // Code taken from https://stackoverflow.com/questions/49506579/how-to-find-the-pid-of-any-process-in-mac-osx-c
+ #include <libproc.h>
+
+ void ProcessResolver::doResolve() {
+ pid_t pids[2048];
+ int bytes = proc_listpids(PROC_ALL_PIDS, 0, pids, sizeof(pids));
+ int n_proc = bytes / sizeof(pids[0]);
+ for (int i = 0; i < n_proc; i++) {
+ struct proc_bsdinfo proc;
+ int st = proc_pidinfo(pids[i], PROC_PIDTBSDINFO, 0,
+ &proc, PROC_PIDTBSDINFO_SIZE);
+ if (st == PROC_PIDTBSDINFO_SIZE) {
+ // add name
+ addName(proc.pbi_name, m_processNames);
+
+ // add corresponding PID
+ m_processPIDs.append(pids[i]);
+ }
+ }
+ }
+#elif defined(Q_OS_FREEBSD)
+ // Implementation for FreeBSD
+ #include <libutil.h>
+ #include <sys/types.h>
+ #include <sys/user.h>
+
+ void ProcessResolver::doResolve() {
+ int n_procs;
+ struct kinfo_proc *procs_info = kinfo_getallproc(&n_procs);
+ if (!procs_info) {
+#ifndef QT_NO_DEBUG
+ qCritical("ProcessResolver: kinfo_getallproc() failed\n");
+#endif
+ return;
+ }
+
+ for (int i = 0; i < n_procs; ++i) {
+ // Add name
+ addName(procs_info[i].ki_comm, m_processNames);
+
+ // Add corresponding PID
+ m_processPIDs.append(procs_info[i].ki_pid);
+ }
+
+ free(procs_info);
+ }
+#elif defined(Q_OS_BSD4)
+ // Implementation of generic BSD other than FreeBSD
+ #include <limits.h>
+
+ #include <fcntl.h>
+ #include <kvm.h>
+ #include <paths.h>
+ #include <sys/sysctl.h>
+ #include <sys/user.h>
+
+ bool kvm_cleanup(kvm_t *kd) {
+ if (kvm_close(kd) == -1) {
+#ifndef QT_NO_DEBUG
+ qCritical("ProcessResolver: kvm_close() failed with error %d\n", errno);
+#endif
+ return false;
+ }
+
+ return true;
+ }
+
+ void ProcessResolver::doResolve() {
+ char error[_POSIX2_LINE_MAX];
+#ifdef KVM_NO_FILES
+ kvm_t *kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, error);
+#else
+ kvm_t *kd = kvm_openfiles(NULL, _PATH_DEVNULL, NULL, O_RDONLY, error);
+#endif
+
+ if (!kd) {
+#ifndef QT_NO_DEBUG
+ qCritical("ProcessResolver: kvm_open2() failed with error: %s\n", error);
+#endif
+ return;
+ }
+
+ int n_procs;
+ struct kinfo_proc *procs_info = kvm_getprocs(kd, KERN_PROC_PROC, 0, &n_procs);
+ if (!procs_info) {
+#ifndef QT_NO_DEBUG
+ qCritical("ProcessResolver: kvm_getprocs() failed\n");
+#endif
+ kvm_cleanup(kd);
+
+ return;
+ }
+
+ for (int i = 0; i < n_procs; ++i) {
+ // Add name
+ addName(procs_info[i].ki_comm, m_processNames);
+
+ // Add corresponding PIDs
+ m_processPIDs.append(procs_info[i].ki_pid);
+ }
+
+ kvm_cleanup(kd);
+ }
+#else
+ #error "No implementation of ProcessResolver::resolve() available for this operating system"
+#endif
diff --git a/src/ProcessResolver.h b/src/ProcessResolver.h
new file mode 100644
index 000000000..59ada3a1c
--- /dev/null
+++ b/src/ProcessResolver.h
@@ -0,0 +1,40 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_PROCESS_RESOLVER_H_
+#define MUMBLE_PROCESS_RESOLVER_H_
+
+#include <stdint.h>
+#include <QtCore/QVector>
+
+/// This ProcessResolver can be used to get a QVector of running process names and associated PIDs on multiple platforms.
+/// This object is by no means thread-safe!
+class ProcessResolver {
+ protected:
+ /// The vector for the pointers to the process names
+ QVector<const char*> m_processNames;
+ /// The vector for the process PIDs
+ QVector<uint64_t> m_processPIDs;
+
+ /// Deletes all names currently stored in processNames and clears processNames and processPIDs
+ void freeAndClearData();
+ /// The OS specific implementation of filling in details about running process names and PIDs
+ void doResolve();
+ public:
+ /// @param resolveImmediately Whether the constructor should directly invoke ProcesResolver::resolve()
+ ProcessResolver(bool resolveImmediately = true);
+ virtual ~ProcessResolver();
+
+ /// Resolves the namaes and PIDs of the running processes
+ void resolve();
+ /// Gets a reference to the stored process names
+ const QVector<const char*>& getProcessNames() const;
+ /// Gets a reference to the stored process PIDs (corresponding to the names returned by ProcessResolver::getProcessNames())
+ const QVector<uint64_t>& getProcessPIDs() const;
+ /// @returns The amount of processes that have been resolved by this object
+ size_t amountOfProcesses() const;
+};
+
+#endif // MUMBLE_PROCESS_RESOLVER_H_
diff --git a/src/mumble/API.h b/src/mumble/API.h
new file mode 100644
index 000000000..74d8196ae
--- /dev/null
+++ b/src/mumble/API.h
@@ -0,0 +1,188 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_API_H_
+#define MUMBLE_MUMBLE_API_H_
+
+// In here the MumbleAPI struct is defined
+#include "MumbleAPI_v_1_0_x.h"
+
+#include <atomic>
+#include <functional>
+#include <future>
+#include <unordered_map>
+
+#include <QObject>
+
+namespace API {
+
+using api_future_t = std::future< mumble_error_t >;
+using api_promise_t = std::promise< mumble_error_t >;
+
+/// A "curator" that will keep track of allocated resources and how to delete them
+struct MumbleAPICurator {
+ struct Entry {
+ /// The function used to delete the corresponding pointer
+ std::function< void(const void *) > m_deleter;
+ /// The ID of the plugin the resource pointed at was allocated for
+ mumble_plugin_id_t m_pluginID;
+ /// The name of the API function the resource pointed to was allocated in
+ /// NOTE: This must only ever be a pointer to a String literal.
+ const char *m_sourceFunctionName;
+ };
+
+ std::unordered_map< const void *, Entry > m_entries;
+
+ ~MumbleAPICurator();
+};
+
+/// This object contains the actual API implementation. It also takes care of synchronizing API calls
+/// with Mumble's main thread so that plugins can call them from an arbitrary thread without causing
+/// issues.
+/// This class is a singleton as a way to be able to write C function wrappers for the member functions
+/// that are needed for passing to the plugins.
+class MumbleAPI : public QObject {
+ Q_OBJECT;
+ Q_DISABLE_COPY(MumbleAPI);
+
+public:
+ static MumbleAPI &get();
+
+public slots:
+ // The description of the functions is provided in MumbleAPI.h
+
+ // Note that every slot is synchronized and is therefore guaranteed to be executed in the main
+ // thread. For the synchronization strategy see below.
+ void freeMemory_v_1_0_x(mumble_plugin_id_t callerID, const void *ptr, api_promise_t *promise);
+ void getActiveServerConnection_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t *connection,
+ api_promise_t *promise);
+ void isConnectionSynchronized_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ bool *synchronized, api_promise_t *promise);
+ void getLocalUserID_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t *userID,
+ api_promise_t *promise);
+ void getUserName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ const char **name, api_promise_t *promise);
+ void getChannelName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **name, api_promise_t *promise);
+ void getAllUsers_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t **users,
+ size_t *userCount, api_promise_t *promise);
+ void getAllChannels_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t **channels, size_t *channelCount, api_promise_t *promise);
+ void getChannelOfUser_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ mumble_channelid_t *channelID, api_promise_t *promise);
+ void getUsersInChannel_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, mumble_userid_t **users, size_t *userCount,
+ api_promise_t *promise);
+ void getLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, mumble_transmission_mode_t *transmissionMode,
+ api_promise_t *promise);
+ void isUserLocallyMuted_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ bool *muted, api_promise_t *promise);
+ void isLocalUserMuted_v_1_0_x(mumble_plugin_id_t callerID, bool *muted, api_promise_t *promise);
+ void isLocalUserDeafened_v_1_0_x(mumble_plugin_id_t callerID, bool *deafened, api_promise_t *promise);
+ void getUserHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ const char **hash, api_promise_t *promise);
+ void getServerHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char **hash,
+ api_promise_t *promise);
+ void requestLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_transmission_mode_t transmissionMode, api_promise_t *promise);
+ void getUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ const char **comment, api_promise_t *promise);
+ void getChannelDescription_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **description, api_promise_t *promise);
+ void requestUserMove_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ mumble_channelid_t channelID, const char *password, api_promise_t *promise);
+ void requestMicrophoneActivationOverwrite_v_1_0_x(mumble_plugin_id_t callerID, bool activate,
+ api_promise_t *promise);
+ void requestLocalMute_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ bool muted, api_promise_t *promise);
+ void requestLocalUserMute_v_1_0_x(mumble_plugin_id_t callerID, bool muted, api_promise_t *promise);
+ void requestLocalUserDeaf_v_1_0_x(mumble_plugin_id_t callerID, bool deafened, api_promise_t *promise);
+ void requestSetLocalUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const char *comment, api_promise_t *promise);
+ void findUserByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char *userName,
+ mumble_userid_t *userID, api_promise_t *promise);
+ void findChannelByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char *channelName,
+ mumble_channelid_t *channelID, api_promise_t *promise);
+ void getMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool *outValue,
+ api_promise_t *promise);
+ void getMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t *outValue,
+ api_promise_t *promise);
+ void getMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, double *outValue,
+ api_promise_t *promise);
+ void getMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, const char **outValue,
+ api_promise_t *promise);
+ void setMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool value,
+ api_promise_t *promise);
+ void setMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t value,
+ api_promise_t *promise);
+ void setMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, double value,
+ api_promise_t *promise);
+ void setMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, const char *value,
+ api_promise_t *promise);
+ void sendData_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const mumble_userid_t *users,
+ size_t userCount, const uint8_t *data, size_t dataLength, const char *dataID,
+ api_promise_t *promise);
+ void log_v_1_0_x(mumble_plugin_id_t callerID, const char *message, api_promise_t *promise);
+ void playSample_v_1_0_x(mumble_plugin_id_t callerID, const char *samplePath, api_promise_t *promise);
+
+
+private:
+ MumbleAPI();
+
+ MumbleAPICurator m_curator;
+};
+
+/// @returns The Mumble API struct (v1.0.x)
+MumbleAPI_v_1_0_x getMumbleAPI_v_1_0_x();
+
+/// Converts from the Qt key-encoding to the API's key encoding.
+///
+/// @param keyCode The Qt key-code that shall be converted
+/// @returns The converted key code or KC_INVALID if conversion failed
+mumble_keycode_t qtKeyCodeToAPIKeyCode(unsigned int keyCode);
+
+/// A class holding non-permanent data set by plugins. Non-permanent means that this data
+/// will not be stored between restarts.
+/// All member field should be atomic in order to be thread-safe
+class PluginData {
+public:
+ /// Constructor
+ PluginData();
+ /// Destructor
+ ~PluginData();
+
+ /// A flag indicating whether a plugin has requested the microphone to be permanently on (mirroring the
+ /// behaviour of the continous transmission mode.
+ std::atomic_bool overwriteMicrophoneActivation;
+
+ /// @returns A reference to the PluginData singleton
+ static PluginData &get();
+}; // class PluginData
+}; // namespace API
+
+
+// Declare the meta-types that we require in order for the API to work
+Q_DECLARE_METATYPE(mumble_settings_key_t);
+Q_DECLARE_METATYPE(mumble_settings_key_t *);
+Q_DECLARE_METATYPE(mumble_transmission_mode_t);
+Q_DECLARE_METATYPE(mumble_transmission_mode_t *);
+Q_DECLARE_METATYPE(API::api_promise_t *);
+
+//////////////////////////////////////////////////////////////
+///////////// SYNCHRONIZATION STRATEGY ///////////////////////
+//////////////////////////////////////////////////////////////
+
+/**
+ * Every API function call checks whether it is being called from the main thread. If it is,
+ * it continues executing as usual. If it is not however, it uses Qt's signal/slot mechanism
+ * to schedule the respective function to be run in the main thread in the next iteration of
+ * the event loop.
+ * In order to synchronize with the calling thread, the return value (error code) of these
+ * functions is "returned" as a promise. Thus by accessing the exit code via the corresponding
+ * future, the calling thread is blocked until the function has been executed in the main thread
+ * (and thereby set the exit code once it is done allowing the calling thread to unblock).
+ */
+
+#endif
diff --git a/src/mumble/API_v_1_0_x.cpp b/src/mumble/API_v_1_0_x.cpp
new file mode 100644
index 000000000..ac8adf6f1
--- /dev/null
+++ b/src/mumble/API_v_1_0_x.cpp
@@ -0,0 +1,1980 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "API.h"
+#include "AudioOutput.h"
+#include "Channel.h"
+#include "ClientUser.h"
+#include "Database.h"
+#include "Log.h"
+#include "MainWindow.h"
+#include "PluginComponents_v_1_0_x.h"
+#include "PluginManager.h"
+#include "ServerHandler.h"
+#include "Settings.h"
+#include "UserModel.h"
+#include "MumbleConstants.h"
+#include "Global.h"
+
+#include <QVariant>
+#include <QtCore/QHash>
+#include <QtCore/QMutex>
+#include <QtCore/QMutexLocker>
+#include <QtCore/QReadLocker>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+
+#include <cstring>
+#include <string>
+
+#define EXIT_WITH(code) \
+ if (promise) { \
+ promise->set_value(code); \
+ } \
+ return;
+
+#define VERIFY_PLUGIN_ID(id) \
+ if (!Global::get().pluginManager->pluginExists(id)) { \
+ EXIT_WITH(EC_INVALID_PLUGIN_ID); \
+ }
+
+// Right now there can only be one connection managed by the current ServerHandler
+#define VERIFY_CONNECTION(connection) \
+ if (!Global::get().sh || Global::get().sh->getConnectionID() != connection) { \
+ EXIT_WITH(EC_CONNECTION_NOT_FOUND); \
+ }
+
+// Right now whether or not a connection has finished synchronizing is indicated by Global::get().uiSession. If it is zero,
+// synchronization is not done yet (or there is no connection to begin with). The connection parameter in the macro is
+// only present in case it will be needed in the future
+#define ENSURE_CONNECTION_SYNCHRONIZED(connection) \
+ if (Global::get().uiSession == 0) { \
+ EXIT_WITH(EC_CONNECTION_UNSYNCHRONIZED); \
+ }
+
+#define UNUSED(var) (void) var;
+
+namespace API {
+
+MumbleAPICurator::~MumbleAPICurator() {
+ // free all remaining resources using the stored deleters
+ for (const auto &current : m_entries) {
+ const Entry &entry = current.second;
+
+ // Delete leaked resource
+ entry.m_deleter(current.first);
+
+ // Print an error about the leaked resource
+ printf("[ERROR]: Plugin with ID %d leaked memory from a call to API function \"%s\"\n", entry.m_pluginID, entry.m_sourceFunctionName);
+ }
+}
+// Some common delete-functions
+void defaultDeleter(const void *ptr) {
+ // We use const-cast in order to circumvent the shortcoming of the free() signature only taking
+ // in void * and not const void *. Delete on the other hand is allowed on const pointers which is
+ // why this is an okay thing to do.
+ // See also https://stackoverflow.com/questions/2819535/unable-to-free-const-pointers-in-c
+ free(const_cast< void * >(ptr));
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////// API IMPLEMENTATION //////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+
+// This macro registers type, type * and type ** to Qt's metatype system
+// and also their const variants (except const value as that doesn't make sense)
+#define REGISTER_METATYPE(type) \
+ qRegisterMetaType< type >(#type); \
+ qRegisterMetaType< type * >(#type " *"); \
+ qRegisterMetaType< type ** >(#type " **"); \
+ qRegisterMetaType< const type * >("const " #type " *"); \
+ qRegisterMetaType< const type ** >("const " #type " **"); \
+
+MumbleAPI::MumbleAPI() {
+ // Move this object to the main thread
+ moveToThread(qApp->thread());
+
+ // Register all API types to Qt's metatype system
+ REGISTER_METATYPE(bool);
+ REGISTER_METATYPE(char);
+ REGISTER_METATYPE(double);
+ REGISTER_METATYPE(int);
+ REGISTER_METATYPE(int64_t);
+ REGISTER_METATYPE(mumble_channelid_t);
+ REGISTER_METATYPE(mumble_connection_t);
+ REGISTER_METATYPE(mumble_plugin_id_t);
+ REGISTER_METATYPE(mumble_settings_key_t);
+ REGISTER_METATYPE(mumble_transmission_mode_t);
+ REGISTER_METATYPE(mumble_userid_t);
+ REGISTER_METATYPE(mumble_userid_t);
+ REGISTER_METATYPE(size_t);
+ REGISTER_METATYPE(uint8_t);
+
+ // Define additional types that can't be defined using macro REGISTER_METATYPE
+ qRegisterMetaType< api_promise_t * >("api_promise_t *");
+ qRegisterMetaType< API::api_promise_t * >("API::api_promise_t *");
+ qRegisterMetaType< const void * >("const void *");
+ qRegisterMetaType< const void ** >("const void **");
+ qRegisterMetaType< void * >("void *");
+ qRegisterMetaType< void ** >("void **");
+}
+
+#undef REFGISTER_METATYPE
+
+MumbleAPI &MumbleAPI::get() {
+ static MumbleAPI api;
+
+ return api;
+}
+
+void MumbleAPI::freeMemory_v_1_0_x(mumble_plugin_id_t callerID, const void *ptr, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "freeMemory_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(const void *, ptr), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ // Don't verify plugin ID here to avoid memory leaks
+ UNUSED(callerID);
+
+ auto it = m_curator.m_entries.find(ptr);
+ if (it != m_curator.m_entries.cend()) {
+ MumbleAPICurator::Entry &entry = (*it).second;
+
+ // call the deleter to delete the resource
+ entry.m_deleter(ptr);
+
+ // Remove pointer from curator
+ m_curator.m_entries.erase(it);
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_POINTER_NOT_FOUND);
+ }
+}
+
+void MumbleAPI::getActiveServerConnection_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t *connection,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getActiveServerConnection_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t *, connection),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ if (Global::get().sh) {
+ *connection = Global::get().sh->getConnectionID();
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_NO_ACTIVE_CONNECTION);
+ }
+}
+
+void MumbleAPI::isConnectionSynchronized_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ bool *synchronized, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "isConnectionSynchronized_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(bool *, synchronized), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+ VERIFY_CONNECTION(connection);
+
+ // Right now there can only be one connection and if Global::get().uiSession is zero, then the synchronization has not finished
+ // yet (or there is no connection to begin with)
+ *synchronized = Global::get().uiSession != 0;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getLocalUserID_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t *userID, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getLocalUserID_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t *, userID), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ *userID = Global::get().uiSession;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getUserName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ const char **name, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getUserName_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, name),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const ClientUser *user = ClientUser::get(userID);
+
+ if (user) {
+ // +1 for NULL terminator
+ size_t size = user->qsName.toUtf8().size() + 1;
+
+ char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(nameArray, user->qsName.toUtf8().data());
+
+ // save the allocated pointer and how to delete it
+ m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getUserName" } });
+
+ *name = nameArray;
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+}
+
+void MumbleAPI::getChannelName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **name, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getChannelName_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_channelid_t, channelID), Q_ARG(const char **, name),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const Channel *channel = Channel::get(channelID);
+
+ if (channel) {
+ // +1 for NULL terminator
+ size_t size = channel->qsName.toUtf8().size() + 1;
+
+ char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(nameArray, channel->qsName.toUtf8().data());
+
+ // save the allocated pointer and how to delete it
+ m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getChannelName" } });
+
+ *name = nameArray;
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_CHANNEL_NOT_FOUND);
+ }
+}
+
+void MumbleAPI::getAllUsers_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t **users, size_t *userCount, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getAllUsers_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t **, users), Q_ARG(size_t *, userCount),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ QReadLocker userLock(&ClientUser::c_qrwlUsers);
+
+ size_t amount = ClientUser::c_qmUsers.size();
+
+ auto it = ClientUser::c_qmUsers.constBegin();
+
+ mumble_userid_t *userIDs = reinterpret_cast< mumble_userid_t * >(malloc(sizeof(mumble_userid_t) * amount));
+
+ unsigned int index = 0;
+ while (it != ClientUser::c_qmUsers.constEnd()) {
+ userIDs[index] = it.key();
+
+ it++;
+ index++;
+ }
+
+ m_curator.m_entries.insert({ userIDs, { defaultDeleter, callerID, "getAllUsers" } });
+
+ *users = userIDs;
+ *userCount = amount;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getAllChannels_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t **channels, size_t *channelCount, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getAllChannels_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_channelid_t **, channels), Q_ARG(size_t *, channelCount),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ QReadLocker channelLock(&Channel::c_qrwlChannels);
+
+ size_t amount = Channel::c_qhChannels.size();
+
+ auto it = Channel::c_qhChannels.constBegin();
+
+ mumble_channelid_t *channelIDs =
+ reinterpret_cast< mumble_channelid_t * >(malloc(sizeof(mumble_channelid_t) * amount));
+
+ unsigned int index = 0;
+ while (it != Channel::c_qhChannels.constEnd()) {
+ channelIDs[index] = it.key();
+
+ it++;
+ index++;
+ }
+
+ m_curator.m_entries.insert({ channelIDs, { defaultDeleter, callerID, "getAllChannels" } });
+
+ *channels = channelIDs;
+ *channelCount = amount;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getChannelOfUser_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, mumble_channelid_t *channelID,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getChannelOfUser_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(mumble_channelid_t *, channelID),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ if (user->cChannel) {
+ *channelID = user->cChannel->iId;
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_GENERIC_ERROR);
+ }
+}
+
+void MumbleAPI::getUsersInChannel_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, mumble_userid_t **users, size_t *userCount,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getUsersInChannel_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_channelid_t, channelID), Q_ARG(mumble_userid_t **, users),
+ Q_ARG(size_t *, userCount), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const Channel *channel = Channel::get(channelID);
+
+ if (!channel) {
+ EXIT_WITH(EC_CHANNEL_NOT_FOUND);
+ }
+
+ size_t amount = channel->qlUsers.size();
+
+ mumble_userid_t *userIDs = reinterpret_cast< mumble_userid_t * >(malloc(sizeof(mumble_userid_t) * amount));
+
+ int index = 0;
+ foreach (const User *currentUser, channel->qlUsers) {
+ userIDs[index] = currentUser->uiSession;
+
+ index++;
+ }
+
+ m_curator.m_entries.insert({ userIDs, { defaultDeleter, callerID, "getUsersInChannel" } });
+
+ *users = userIDs;
+ *userCount = amount;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_transmission_mode_t *transmissionMode,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(
+ this, "getLocalUserTransmissionMode_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(mumble_transmission_mode_t *, transmissionMode), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ switch (Global::get().s.atTransmit) {
+ case Settings::AudioTransmit::Continuous:
+ *transmissionMode = TM_CONTINOUS;
+ EXIT_WITH(STATUS_OK);
+ case Settings::AudioTransmit::VAD:
+ *transmissionMode = TM_VOICE_ACTIVATION;
+ EXIT_WITH(STATUS_OK);
+ case Settings::AudioTransmit::PushToTalk:
+ *transmissionMode = TM_PUSH_TO_TALK;
+ EXIT_WITH(STATUS_OK);
+ }
+
+ // Unable to resolve transmission mode
+ EXIT_WITH(EC_GENERIC_ERROR);
+}
+
+void MumbleAPI::isUserLocallyMuted_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, bool *muted, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "isUserLocallyMuted_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(bool *, muted),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ *muted = user->bLocalMute;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::isLocalUserMuted_v_1_0_x(mumble_plugin_id_t callerID, bool *muted, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "isLocalUserMuted_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool *, muted),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ *muted = Global::get().s.bMute;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::isLocalUserDeafened_v_1_0_x(mumble_plugin_id_t callerID, bool *deafened, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "isLocalUserDeafened_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool *, deafened),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ *deafened = Global::get().s.bDeaf;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getUserHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID,
+ const char **hash, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getUserHash_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, hash),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ // The user's hash is already in hexadecimal representation, so we don't have to worry about null-bytes in it
+ // +1 for NULL terminator
+ size_t size = user->qsHash.toUtf8().size() + 1;
+
+ char *hashArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(hashArray, user->qsHash.toUtf8().data());
+
+ m_curator.m_entries.insert({ hashArray, { defaultDeleter, callerID, "getUserHash" } });
+
+ *hash = hashArray;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getServerHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char **hash,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getServerHash_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(const char **, hash), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ // Use hexadecimal representation in order for the String to be properly printable and for it to be C-encodable
+ QByteArray hashHex = Global::get().sh->qbaDigest.toHex();
+ QString strHash = QString::fromLatin1(hashHex);
+
+ // +1 for NULL terminator
+ size_t size = strHash.toUtf8().size() + 1;
+
+ char *hashArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(hashArray, strHash.toUtf8().data());
+
+ m_curator.m_entries.insert({ hashArray, { defaultDeleter, callerID, "getServerHash" } });
+
+ *hash = hashArray;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_transmission_mode_t transmissionMode,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestLocalUserTransmissionMode_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(mumble_transmission_mode_t, transmissionMode), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ switch (transmissionMode) {
+ case TM_CONTINOUS:
+ Global::get().s.atTransmit = Settings::AudioTransmit::Continuous;
+ EXIT_WITH(STATUS_OK);
+ case TM_VOICE_ACTIVATION:
+ Global::get().s.atTransmit = Settings::AudioTransmit::VAD;
+ EXIT_WITH(STATUS_OK);
+ case TM_PUSH_TO_TALK:
+ Global::get().s.atTransmit = Settings::AudioTransmit::PushToTalk;
+ EXIT_WITH(STATUS_OK);
+ }
+
+ EXIT_WITH(EC_UNKNOWN_TRANSMISSION_MODE);
+}
+
+void MumbleAPI::getUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, const char **comment, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getUserComment_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, comment),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ if (user->qsComment.isEmpty() && !user->qbaCommentHash.isEmpty()) {
+ user->qsComment = QString::fromUtf8(Global::get().db->blob(user->qbaCommentHash));
+
+ if (user->qsComment.isEmpty()) {
+ // The user's comment hasn't been synchronized to this client yet
+ EXIT_WITH(EC_UNSYNCHRONIZED_BLOB);
+ }
+ }
+
+ // +1 for NULL terminator
+ size_t size = user->qsComment.toUtf8().size() + 1;
+
+ char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(nameArray, user->qsComment.toUtf8().data());
+
+ m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getUserComment" } });
+
+ *comment = nameArray;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getChannelDescription_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **description,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getChannelDescription_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_channelid_t, channelID), Q_ARG(const char **, description),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ Channel *channel = Channel::get(channelID);
+
+ if (!channel) {
+ EXIT_WITH(EC_CHANNEL_NOT_FOUND);
+ }
+
+ if (channel->qsDesc.isEmpty() && !channel->qbaDescHash.isEmpty()) {
+ channel->qsDesc = QString::fromUtf8(Global::get().db->blob(channel->qbaDescHash));
+
+ if (channel->qsDesc.isEmpty()) {
+ // The channel's description hasn't been synchronized to this client yet
+ EXIT_WITH(EC_UNSYNCHRONIZED_BLOB);
+ }
+ }
+
+ // +1 for NULL terminator
+ size_t size = channel->qsDesc.toUtf8().size() + 1;
+
+ char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(nameArray, channel->qsDesc.toUtf8().data());
+
+ m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getChannelDescription" } });
+
+ *description = nameArray;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestUserMove_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, mumble_channelid_t channelID, const char *password,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestUserMove_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(mumble_channelid_t, channelID),
+ Q_ARG(const char *, password), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ const Channel *channel = Channel::get(channelID);
+
+ if (!channel) {
+ EXIT_WITH(EC_CHANNEL_NOT_FOUND);
+ }
+
+ if (channel != user->cChannel) {
+ // send move-request to the server only if the user is not in the channel already
+ QStringList passwordList;
+ if (password) {
+ passwordList << QString::fromUtf8(password);
+ }
+
+ Global::get().sh->joinChannel(user->uiSession, channel->iId, passwordList);
+ }
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestMicrophoneActivationOverwrite_v_1_0_x(mumble_plugin_id_t callerID, bool activate,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestMicrophoneActivationOverwrite_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, activate),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ PluginData::get().overwriteMicrophoneActivation.store(activate);
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestLocalMute_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ mumble_userid_t userID, bool muted, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestLocalMute_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(mumble_userid_t, userID), Q_ARG(bool, muted), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ if (userID == Global::get().uiSession) {
+ // Can't locally mute the local user
+ EXIT_WITH(EC_INVALID_MUTE_TARGET);
+ }
+
+ ClientUser *user = ClientUser::get(userID);
+
+ if (!user) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ user->setLocalMute(muted);
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestLocalUserMute_v_1_0_x(mumble_plugin_id_t callerID, bool muted, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestLocalUserMute_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, muted),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ if (!Global::get().mw) {
+ EXIT_WITH(EC_INTERNAL_ERROR);
+ }
+
+ Global::get().mw->setAudioMute(muted);
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestLocalUserDeaf_v_1_0_x(mumble_plugin_id_t callerID, bool deafened, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestLocalUserDeaf_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, deafened),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ if (!Global::get().mw) {
+ EXIT_WITH(EC_INTERNAL_ERROR);
+ }
+
+ Global::get().mw->setAudioDeaf(deafened);
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::requestSetLocalUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const char *comment, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "requestSetLocalUserComment_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(const char *, comment), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ ClientUser *localUser = ClientUser::get(Global::get().uiSession);
+
+ if (!localUser) {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+
+ if (!Global::get().mw || !Global::get().mw->pmModel) {
+ EXIT_WITH(EC_INTERNAL_ERROR);
+ }
+
+ Global::get().mw->pmModel->setComment(localUser, QString::fromUtf8(comment));
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::findUserByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const char *userName, mumble_userid_t *userID, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "findUserByName_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(const char *, userName), Q_ARG(mumble_userid_t *, userID),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const QString qsUserName = QString::fromUtf8(userName);
+
+ QReadLocker userLock(&ClientUser::c_qrwlUsers);
+
+ auto it = ClientUser::c_qmUsers.constBegin();
+ while (it != ClientUser::c_qmUsers.constEnd()) {
+ if (it.value()->qsName == qsUserName) {
+ *userID = it.key();
+
+ EXIT_WITH(STATUS_OK);
+ }
+
+ it++;
+ }
+
+ EXIT_WITH(EC_USER_NOT_FOUND);
+}
+
+void MumbleAPI::findChannelByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const char *channelName, mumble_channelid_t *channelID,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "findChannelByName_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection),
+ Q_ARG(const char *, channelName), Q_ARG(mumble_channelid_t *, channelID),
+ Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ const QString qsChannelName = QString::fromUtf8(channelName);
+
+ QReadLocker channelLock(&Channel::c_qrwlChannels);
+
+ auto it = Channel::c_qhChannels.constBegin();
+ while (it != Channel::c_qhChannels.constEnd()) {
+ if (it.value()->qsName == qsChannelName) {
+ *channelID = it.key();
+
+ EXIT_WITH(STATUS_OK);
+ }
+
+ it++;
+ }
+
+ EXIT_WITH(EC_CHANNEL_NOT_FOUND);
+}
+
+QVariant getMumbleSettingHelper(mumble_settings_key_t key) {
+ QVariant value;
+
+ // All values are explicitly cast to the target type of their associated API. For instance there is not API to
+ // get float values but there is one for doubles. Therefore floats have to be cast to doubles in order for the
+ // type checking to work out.
+ switch (key) {
+ case MSK_AUDIO_INPUT_VOICE_HOLD:
+ value = static_cast< int >(Global::get().s.iVoiceHold);
+ break;
+ case MSK_AUDIO_INPUT_VAD_SILENCE_THRESHOLD:
+ value = static_cast< double >(Global::get().s.fVADmin);
+ break;
+ case MSK_AUDIO_INPUT_VAD_SPEECH_THRESHOLD:
+ value = static_cast< double >(Global::get().s.fVADmax);
+ break;
+ case MSK_AUDIO_OUTPUT_PA_MINIMUM_DISTANCE:
+ value = static_cast< double >(Global::get().s.fAudioMinDistance);
+ break;
+ case MSK_AUDIO_OUTPUT_PA_MAXIMUM_DISTANCE:
+ value = static_cast< double >(Global::get().s.fAudioMaxDistance);
+ break;
+ case MSK_AUDIO_OUTPUT_PA_BLOOM:
+ value = static_cast< double >(Global::get().s.fAudioBloom);
+ break;
+ case MSK_AUDIO_OUTPUT_PA_MINIMUM_VOLUME:
+ value = static_cast< double >(Global::get().s.fAudioMaxDistVolume);
+ break;
+ case MSK_INVALID:
+ // There is no setting associated with this key
+ break;
+ }
+
+ return value;
+}
+
+// IS_TYPE actually only checks if the QVariant can be converted to the needed type since that's all that we really care
+// about at the end of the day.
+#define IS_TYPE(var, varType) static_cast< QMetaType::Type >(var.type()) == varType
+#define IS_NOT_TYPE(var, varType) static_cast< QMetaType::Type >(var.type()) != varType
+
+void MumbleAPI::getMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool *outValue,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getMumbleSetting_bool_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(bool *, outValue), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ QVariant value = getMumbleSettingHelper(key);
+
+ if (!value.isValid()) {
+ // We also return that for MSK_INVALID
+ EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY);
+ }
+
+ if (IS_NOT_TYPE(value, QMetaType::Bool)) {
+ EXIT_WITH(EC_WRONG_SETTINGS_TYPE);
+ }
+
+ *outValue = value.toBool();
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t *outValue,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getMumbleSetting_int_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(int64_t *, outValue), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ QVariant value = getMumbleSettingHelper(key);
+
+ if (!value.isValid()) {
+ // We also return that for MSK_INVALID
+ EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY);
+ }
+
+ if (IS_NOT_TYPE(value, QMetaType::Int)) {
+ EXIT_WITH(EC_WRONG_SETTINGS_TYPE);
+ }
+
+ *outValue = value.toInt();
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key,
+ double *outValue, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getMumbleSetting_double_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(double *, outValue), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ QVariant value = getMumbleSettingHelper(key);
+
+ if (!value.isValid()) {
+ // We also return that for MSK_INVALID
+ EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY);
+ }
+
+ if (IS_NOT_TYPE(value, QMetaType::Double)) {
+ EXIT_WITH(EC_WRONG_SETTINGS_TYPE);
+ }
+
+ *outValue = value.toDouble();
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::getMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key,
+ const char **outValue, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "getMumbleSetting_string_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(const char **, outValue), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ QVariant value = getMumbleSettingHelper(key);
+
+ if (!value.isValid()) {
+ // We also return that for MSK_INVALID
+ EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY);
+ }
+
+ if (IS_NOT_TYPE(value, QMetaType::QString)) {
+ EXIT_WITH(EC_WRONG_SETTINGS_TYPE);
+ }
+
+ const QString stringValue = value.toString();
+
+ // +1 for NULL terminator
+ size_t size = stringValue.toUtf8().size() + 1;
+
+ char *valueArray = reinterpret_cast< char * >(malloc(size * sizeof(char)));
+
+ std::strcpy(valueArray, stringValue.toUtf8().data());
+
+ m_curator.m_entries.insert({ valueArray, { defaultDeleter, callerID, "getMumbleSetting_string" } });
+
+ *outValue = valueArray;
+
+ EXIT_WITH(STATUS_OK);
+}
+
+mumble_error_t setMumbleSettingHelper(mumble_settings_key_t key, QVariant value) {
+ switch (key) {
+ case MSK_AUDIO_INPUT_VOICE_HOLD:
+ if (IS_TYPE(value, QMetaType::Int)) {
+ Global::get().s.iVoiceHold = value.toInt();
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_INPUT_VAD_SILENCE_THRESHOLD:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fVADmin = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_INPUT_VAD_SPEECH_THRESHOLD:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fVADmax = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_OUTPUT_PA_MINIMUM_DISTANCE:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fAudioMinDistance = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_OUTPUT_PA_MAXIMUM_DISTANCE:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fAudioMaxDistance = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_OUTPUT_PA_BLOOM:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fAudioBloom = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_AUDIO_OUTPUT_PA_MINIMUM_VOLUME:
+ if (IS_TYPE(value, QMetaType::Double)) {
+ Global::get().s.fAudioMaxDistVolume = static_cast< float >(value.toDouble());
+
+ return STATUS_OK;
+ } else {
+ return EC_WRONG_SETTINGS_TYPE;
+ }
+ case MSK_INVALID:
+ // Do nothing
+ break;
+ }
+
+ return EC_UNKNOWN_SETTINGS_KEY;
+}
+
+void MumbleAPI::setMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool value,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "setMumbleSetting_bool_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(bool, value), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ mumble_error_t exitCode = setMumbleSettingHelper(key, value);
+ EXIT_WITH(exitCode);
+}
+
+void MumbleAPI::setMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t value,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "setMumbleSetting_int_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(int64_t, value), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ mumble_error_t exitCode = setMumbleSettingHelper(key, QVariant::fromValue(value));
+ EXIT_WITH(exitCode);
+}
+
+void MumbleAPI::setMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, double value,
+ api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "setMumbleSetting_double_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(double, value), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ mumble_error_t exitCode = setMumbleSettingHelper(key, value);
+ EXIT_WITH(exitCode);
+}
+
+void MumbleAPI::setMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key,
+ const char *value, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "setMumbleSetting_string_v_1_0_x", Qt::QueuedConnection,
+ Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key),
+ Q_ARG(const char *, value), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ mumble_error_t exitCode = setMumbleSettingHelper(key, QString::fromUtf8(value));
+ EXIT_WITH(exitCode);
+}
+#undef IS_TYPE
+#undef IS_NOT_TYPE
+
+void MumbleAPI::sendData_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const mumble_userid_t *users, size_t userCount, const uint8_t *data, size_t dataLength,
+ const char *dataID, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "sendData_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(mumble_connection_t, connection), Q_ARG(const mumble_userid_t *, users),
+ Q_ARG(size_t, userCount), Q_ARG(const uint8_t *, data), Q_ARG(size_t, dataLength),
+ Q_ARG(const char *, dataID), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ VERIFY_CONNECTION(connection);
+ ENSURE_CONNECTION_SYNCHRONIZED(connection);
+
+ if (dataLength > Mumble::Plugins::PluginMessage::MAX_DATA_LENGTH) {
+ EXIT_WITH(EC_DATA_TOO_BIG);
+ }
+ if (std::strlen(dataID) > Mumble::Plugins::PluginMessage::MAX_DATA_ID_LENGTH) {
+ EXIT_WITH(EC_DATA_ID_TOO_LONG);
+ }
+
+ MumbleProto::PluginDataTransmission mpdt;
+ mpdt.set_sendersession(Global::get().uiSession);
+
+ for (size_t i = 0; i < userCount; i++) {
+ const ClientUser *user = ClientUser::get(users[i]);
+
+ if (user) {
+ mpdt.add_receiversessions(users[i]);
+ } else {
+ EXIT_WITH(EC_USER_NOT_FOUND);
+ }
+ }
+
+ mpdt.set_data(data, dataLength);
+ mpdt.set_dataid(dataID);
+
+ if (Global::get().sh) {
+ Global::get().sh->sendMessage(mpdt);
+
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_CONNECTION_NOT_FOUND);
+ }
+}
+
+void MumbleAPI::log_v_1_0_x(mumble_plugin_id_t callerID, const char *message, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "log_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(const char *, message), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ // We verify the plugin manually as we need a handle to it later
+ const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(callerID);
+ if (!plugin) {
+ EXIT_WITH(EC_INVALID_PLUGIN_ID);
+ }
+
+ QString msg = QString::fromLatin1("<b>%1:</b> %2")
+ .arg(plugin->getName().toHtmlEscaped())
+ .arg(QString::fromUtf8(message).toHtmlEscaped());
+
+ // Use static method that handles the case in which the Log object doesn't exist yet
+ Log::logOrDefer(Log::PluginMessage, msg);
+
+ EXIT_WITH(STATUS_OK);
+}
+
+void MumbleAPI::playSample_v_1_0_x(mumble_plugin_id_t callerID, const char *samplePath, api_promise_t *promise) {
+ if (QThread::currentThread() != thread()) {
+ // Invoke in main thread
+ QMetaObject::invokeMethod(this, "playSample_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID),
+ Q_ARG(const char *, samplePath), Q_ARG(api_promise_t *, promise));
+
+ return;
+ }
+
+ VERIFY_PLUGIN_ID(callerID);
+
+ if (!Global::get().ao) {
+ EXIT_WITH(EC_AUDIO_NOT_AVAILABLE);
+ }
+
+ if (Global::get().ao->playSample(QString::fromUtf8(samplePath), false)) {
+ EXIT_WITH(STATUS_OK);
+ } else {
+ EXIT_WITH(EC_INVALID_SAMPLE);
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////// C FUNCTION WRAPPERS FOR USE IN API STRUCT ///////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+
+mumble_error_t PLUGIN_CALLING_CONVENTION freeMemory_v_1_0_x(mumble_plugin_id_t callerID, const void *ptr) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().freeMemory_v_1_0_x(callerID, ptr, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getActiveServerConnection_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t *connection) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getActiveServerConnection_v_1_0_x(callerID, connection, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION isConnectionSynchronized_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ bool *synchronized) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().isConnectionSynchronized_v_1_0_x(callerID, connection, synchronized, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getLocalUserID_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_userid_t *userID) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getLocalUserID_v_1_0_x(callerID, connection, userID, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getUserName_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, mumble_userid_t userID,
+ const char **name) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getUserName_v_1_0_x(callerID, connection, userID, name, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getChannelName_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_channelid_t channelID, const char **name) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getChannelName_v_1_0_x(callerID, connection, channelID, name, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getAllUsers_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, mumble_userid_t **users,
+ size_t *userCount) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getAllUsers_v_1_0_x(callerID, connection, users, userCount, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getAllChannels_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_channelid_t **channels, size_t *channelCount) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getAllChannels_v_1_0_x(callerID, connection, channels, channelCount, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getChannelOfUser_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_userid_t userID, mumble_channelid_t *channel) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getChannelOfUser_v_1_0_x(callerID, connection, userID, channel, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getUsersInChannel_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_channelid_t channelID,
+ mumble_userid_t **userList, size_t *userCount) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getUsersInChannel_v_1_0_x(callerID, connection, channelID, userList, userCount, &promise);
+
+ return future.get();
+}
+
+
+mumble_error_t PLUGIN_CALLING_CONVENTION
+ getLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, mumble_transmission_mode_t *transmissionMode) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getLocalUserTransmissionMode_v_1_0_x(callerID, transmissionMode, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION isUserLocallyMuted_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_userid_t userID, bool *muted) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().isUserLocallyMuted_v_1_0_x(callerID, connection, userID, muted, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION isLocalUserMuted_v_1_0_x(mumble_plugin_id_t callerID, bool *muted) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().isLocalUserMuted_v_1_0_x(callerID, muted, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION isLocalUserDeafened_v_1_0_x(mumble_plugin_id_t callerID, bool *deafened) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().isLocalUserDeafened_v_1_0_x(callerID, deafened, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getUserHash_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, mumble_userid_t userID,
+ const char **hash) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getUserHash_v_1_0_x(callerID, connection, userID, hash, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getServerHash_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, const char **hash) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getServerHash_v_1_0_x(callerID, connection, hash, &promise);
+
+ return future.get();
+}
+
+
+mumble_error_t PLUGIN_CALLING_CONVENTION
+ requestLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, mumble_transmission_mode_t transmissionMode) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestLocalUserTransmissionMode_v_1_0_x(callerID, transmissionMode, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getUserComment_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, mumble_userid_t userID,
+ const char **comment) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getUserComment_v_1_0_x(callerID, connection, userID, comment, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getChannelDescription_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_channelid_t channelID,
+ const char **description) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getChannelDescription_v_1_0_x(callerID, connection, channelID, description, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestUserMove_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, mumble_userid_t userID,
+ mumble_channelid_t channelID, const char *password) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestUserMove_v_1_0_x(callerID, connection, userID, channelID, password, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestMicrophoneActivationOverwrite_v_1_0_x(mumble_plugin_id_t callerID,
+ bool activate) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestMicrophoneActivationOverwrite_v_1_0_x(callerID, activate, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestLocalMute_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ mumble_userid_t userID, bool muted) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestLocalMute_v_1_0_x(callerID, connection, userID, muted, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestLocalUserMute_v_1_0_x(mumble_plugin_id_t callerID, bool muted) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestLocalUserMute_v_1_0_x(callerID, muted, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestLocalUserDeaf_v_1_0_x(mumble_plugin_id_t callerID, bool deafened) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestLocalUserDeaf_v_1_0_x(callerID, deafened, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION requestSetLocalUserComment_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ const char *comment) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().requestSetLocalUserComment_v_1_0_x(callerID, connection, comment, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION findUserByName_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection, const char *userName,
+ mumble_userid_t *userID) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().findUserByName_v_1_0_x(callerID, connection, userName, userID, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION findChannelByName_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_connection_t connection,
+ const char *channelName,
+ mumble_channelid_t *channelID) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().findChannelByName_v_1_0_x(callerID, connection, channelName, channelID, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, bool *outValue) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getMumbleSetting_bool_v_1_0_x(callerID, key, outValue, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, int64_t *outValue) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getMumbleSetting_int_v_1_0_x(callerID, key, outValue, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, double *outValue) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getMumbleSetting_double_v_1_0_x(callerID, key, outValue, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key,
+ const char **outValue) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().getMumbleSetting_string_v_1_0_x(callerID, key, outValue, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, bool value) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().setMumbleSetting_bool_v_1_0_x(callerID, key, value, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, int64_t value) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().setMumbleSetting_int_v_1_0_x(callerID, key, value, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, double value) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().setMumbleSetting_double_v_1_0_x(callerID, key, value, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID,
+ mumble_settings_key_t key, const char *value) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().setMumbleSetting_string_v_1_0_x(callerID, key, value, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION sendData_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection,
+ const mumble_userid_t *users, size_t userCount,
+ const uint8_t *data, size_t dataLength, const char *dataID) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().sendData_v_1_0_x(callerID, connection, users, userCount, data, dataLength, dataID, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION log_v_1_0_x(mumble_plugin_id_t callerID, const char *message) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().log_v_1_0_x(callerID, message, &promise);
+
+ return future.get();
+}
+
+mumble_error_t PLUGIN_CALLING_CONVENTION playSample_v_1_0_x(mumble_plugin_id_t callerID, const char *samplePath) {
+ api_promise_t promise;
+ api_future_t future = promise.get_future();
+
+ MumbleAPI::get().playSample_v_1_0_x(callerID, samplePath, &promise);
+
+ return future.get();
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////// GETTER FOR API STRUCTS /////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+
+MumbleAPI_v_1_0_x getMumbleAPI_v_1_0_x() {
+ return { freeMemory_v_1_0_x,
+ getActiveServerConnection_v_1_0_x,
+ isConnectionSynchronized_v_1_0_x,
+ getLocalUserID_v_1_0_x,
+ getUserName_v_1_0_x,
+ getChannelName_v_1_0_x,
+ getAllUsers_v_1_0_x,
+ getAllChannels_v_1_0_x,
+ getChannelOfUser_v_1_0_x,
+ getUsersInChannel_v_1_0_x,
+ getLocalUserTransmissionMode_v_1_0_x,
+ isUserLocallyMuted_v_1_0_x,
+ isLocalUserMuted_v_1_0_x,
+ isLocalUserDeafened_v_1_0_x,
+ getUserHash_v_1_0_x,
+ getServerHash_v_1_0_x,
+ getUserComment_v_1_0_x,
+ getChannelDescription_v_1_0_x,
+ requestLocalUserTransmissionMode_v_1_0_x,
+ requestUserMove_v_1_0_x,
+ requestMicrophoneActivationOverwrite_v_1_0_x,
+ requestLocalMute_v_1_0_x,
+ requestLocalUserMute_v_1_0_x,
+ requestLocalUserDeaf_v_1_0_x,
+ requestSetLocalUserComment_v_1_0_x,
+ findUserByName_v_1_0_x,
+ findChannelByName_v_1_0_x,
+ getMumbleSetting_bool_v_1_0_x,
+ getMumbleSetting_int_v_1_0_x,
+ getMumbleSetting_double_v_1_0_x,
+ getMumbleSetting_string_v_1_0_x,
+ setMumbleSetting_bool_v_1_0_x,
+ setMumbleSetting_int_v_1_0_x,
+ setMumbleSetting_double_v_1_0_x,
+ setMumbleSetting_string_v_1_0_x,
+ sendData_v_1_0_x,
+ log_v_1_0_x,
+ playSample_v_1_0_x };
+}
+
+#define MAP(qtName, apiName) \
+ case Qt::Key_##qtName: \
+ return KC_##apiName
+
+mumble_keycode_t qtKeyCodeToAPIKeyCode(unsigned int keyCode) {
+ switch (keyCode) {
+ MAP(Escape, ESCAPE);
+ MAP(Tab, TAB);
+ MAP(Backspace, BACKSPACE);
+ case Qt::Key_Return:
+ // Fallthrough
+ case Qt::Key_Enter:
+ return KC_ENTER;
+ MAP(Delete, DELETE);
+ MAP(Print, PRINT);
+ MAP(Home, HOME);
+ MAP(End, END);
+ MAP(Up, UP);
+ MAP(Down, DOWN);
+ MAP(Left, LEFT);
+ MAP(Right, RIGHT);
+ MAP(PageUp, PAGE_UP);
+ MAP(PageDown, PAGE_DOWN);
+ MAP(Shift, SHIFT);
+ MAP(Control, CONTROL);
+ MAP(Meta, META);
+ MAP(Alt, ALT);
+ MAP(AltGr, ALT_GR);
+ MAP(CapsLock, CAPSLOCK);
+ MAP(NumLock, NUMLOCK);
+ MAP(ScrollLock, SCROLLLOCK);
+ MAP(F1, F1);
+ MAP(F2, F2);
+ MAP(F3, F3);
+ MAP(F4, F4);
+ MAP(F5, F5);
+ MAP(F6, F6);
+ MAP(F7, F7);
+ MAP(F8, F8);
+ MAP(F9, F9);
+ MAP(F10, F10);
+ MAP(F11, F11);
+ MAP(F12, F12);
+ MAP(F13, F13);
+ MAP(F14, F14);
+ MAP(F15, F15);
+ MAP(F16, F16);
+ MAP(F17, F17);
+ MAP(F18, F18);
+ MAP(F19, F19);
+ case Qt::Key_Super_L:
+ // Fallthrough
+ case Qt::Key_Super_R:
+ return KC_SUPER;
+ MAP(Space, SPACE);
+ MAP(Exclam, EXCLAMATION_MARK);
+ MAP(QuoteDbl, DOUBLE_QUOTE);
+ MAP(NumberSign, HASHTAG);
+ MAP(Dollar, DOLLAR);
+ MAP(Percent, PERCENT);
+ MAP(Ampersand, AMPERSAND);
+ MAP(Apostrophe, SINGLE_QUOTE);
+ MAP(ParenLeft, OPEN_PARENTHESIS);
+ MAP(ParenRight, CLOSE_PARENTHESIS);
+ MAP(Asterisk, ASTERISK);
+ MAP(Plus, PLUS);
+ MAP(Comma, COMMA);
+ MAP(Minus, MINUS);
+ MAP(Period, PERIOD);
+ MAP(Slash, SLASH);
+ MAP(0, 0);
+ MAP(1, 1);
+ MAP(2, 2);
+ MAP(3, 3);
+ MAP(4, 4);
+ MAP(5, 5);
+ MAP(6, 6);
+ MAP(7, 7);
+ MAP(8, 8);
+ MAP(9, 9);
+ MAP(Colon, COLON);
+ MAP(Semicolon, SEMICOLON);
+ MAP(Less, LESS_THAN);
+ MAP(Equal, EQUALS);
+ MAP(Greater, GREATER_THAN);
+ MAP(Question, QUESTION_MARK);
+ MAP(At, AT_SYMBOL);
+ MAP(A, A);
+ MAP(B, B);
+ MAP(C, C);
+ MAP(D, D);
+ MAP(E, E);
+ MAP(F, F);
+ MAP(G, G);
+ MAP(H, H);
+ MAP(I, I);
+ MAP(J, J);
+ MAP(K, K);
+ MAP(L, L);
+ MAP(M, M);
+ MAP(N, N);
+ MAP(O, O);
+ MAP(P, P);
+ MAP(Q, Q);
+ MAP(R, R);
+ MAP(S, S);
+ MAP(T, T);
+ MAP(U, U);
+ MAP(V, V);
+ MAP(W, W);
+ MAP(X, X);
+ MAP(Y, Y);
+ MAP(Z, Z);
+ MAP(BracketLeft, OPEN_BRACKET);
+ MAP(BracketRight, CLOSE_BRACKET);
+ MAP(Backslash, BACKSLASH);
+ MAP(AsciiCircum, CIRCUMFLEX);
+ MAP(Underscore, UNDERSCORE);
+ MAP(BraceLeft, OPEN_BRACE);
+ MAP(BraceRight, CLOSE_BRACE);
+ MAP(Bar, VERTICAL_BAR);
+ MAP(AsciiTilde, TILDE);
+ MAP(degree, DEGREE_SIGN);
+ }
+
+ return KC_INVALID;
+}
+
+#undef MAP
+
+
+// Implementation of PluginData
+PluginData::PluginData() : overwriteMicrophoneActivation(false) {
+}
+
+PluginData::~PluginData() {
+}
+
+PluginData &PluginData::get() {
+ static PluginData *instance = new PluginData();
+
+ return *instance;
+}
+}; // namespace API
+
+#undef EXIT_WITH
+#undef VERIFY_PLUGIN_ID
+#undef VERIFY_CONNECTION
+#undef ENSURE_CONNECTION_SYNCHRONIZED
+#undef UNUSED
diff --git a/src/mumble/Audio.cpp b/src/mumble/Audio.cpp
index 9d383ffe7..b2d61025d 100644
--- a/src/mumble/Audio.cpp
+++ b/src/mumble/Audio.cpp
@@ -13,6 +13,7 @@
#endif
#include "Log.h"
#include "PacketDataStream.h"
+#include "PluginManager.h"
#include "Global.h"
#include <QtCore/QObject>
@@ -269,6 +270,15 @@ void Audio::stopInput() {
void Audio::start(const QString &input, const QString &output) {
startInput(input);
startOutput(output);
+
+ // Now that the audio input and output is created, we connect them to the PluginManager
+ // As these callbacks might want to change the audio before it gets further processed, all these connections have to be direct
+ QObject::connect(Global::get().ai.get(), &AudioInput::audioInputEncountered, Global::get().pluginManager,
+ &PluginManager::on_audioInput, Qt::DirectConnection);
+ QObject::connect(Global::get().ao.get(), &AudioOutput::audioSourceFetched, Global::get().pluginManager,
+ &PluginManager::on_audioSourceFetched, Qt::DirectConnection);
+ QObject::connect(Global::get().ao.get(), &AudioOutput::audioOutputAboutToPlay, Global::get().pluginManager,
+ &PluginManager::on_audioOutputAboutToPlay, Qt::DirectConnection);
}
void Audio::stop() {
diff --git a/src/mumble/AudioInput.cpp b/src/mumble/AudioInput.cpp
index d898ca0cc..cf1ca3820 100644
--- a/src/mumble/AudioInput.cpp
+++ b/src/mumble/AudioInput.cpp
@@ -11,14 +11,18 @@
# include "OpusCodec.h"
#endif
#include "MainWindow.h"
+#include "User.h"
+#include "PacketDataStream.h"
+#include "PluginManager.h"
#include "Message.h"
#include "NetworkConfig.h"
#include "PacketDataStream.h"
-#include "Plugins.h"
#include "ServerHandler.h"
#include "User.h"
#include "Utils.h"
#include "VoiceRecorder.h"
+#include "API.h"
+
#include "Global.h"
#ifdef USE_RNNOISE
@@ -1058,7 +1062,7 @@ void AudioInput::encodeAudioFrame(AudioChunk chunk) {
iHoldFrames = 0;
}
- if (Global::get().s.atTransmit == Settings::Continuous) {
+ if (Global::get().s.atTransmit == Settings::Continuous || API::PluginData::get().overwriteMicrophoneActivation.load()) {
// Continous transmission is enabled
bIsSpeech = true;
} else if (Global::get().s.atTransmit == Settings::PushToTalk) {
@@ -1143,6 +1147,8 @@ void AudioInput::encodeAudioFrame(AudioChunk chunk) {
EncodingOutputBuffer buffer;
Q_ASSERT(buffer.size() >= static_cast< size_t >(iAudioQuality / 100 * iAudioFrames / 8));
+ emit audioInputEncountered(psSource, iFrameSize, iMicChannels, SAMPLE_RATE, bIsSpeech);
+
int len = 0;
bool encoded = true;
@@ -1274,10 +1280,13 @@ void AudioInput::flushCheck(const QByteArray &frame, bool terminator, int voiceT
}
}
- if (Global::get().s.bTransmitPosition && Global::get().p && !Global::get().bCenterPosition && Global::get().p->fetch()) {
- pds << Global::get().p->fPosition[0];
- pds << Global::get().p->fPosition[1];
- pds << Global::get().p->fPosition[2];
+ if (Global::get().s.bTransmitPosition && Global::get().pluginManager && !Global::get().bCenterPosition
+ && Global::get().pluginManager->fetchPositionalData()) {
+ Position3D currentPos = Global::get().pluginManager->getPositionalData().getPlayerPos();
+
+ pds << currentPos.x;
+ pds << currentPos.y;
+ pds << currentPos.z;
}
sendAudioFrame(data, pds);
diff --git a/src/mumble/AudioInput.h b/src/mumble/AudioInput.h
index b9dbb0c4e..0410db421 100644
--- a/src/mumble/AudioInput.h
+++ b/src/mumble/AudioInput.h
@@ -254,6 +254,14 @@ protected:
signals:
void doDeaf();
void doMute();
+ /// A signal emitted if audio input is being encountered
+ ///
+ /// @param inputPCM The encountered input PCM
+ /// @param sampleCount The amount of samples in the input
+ /// @param channelCount The amount of channels in the input
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers the inpu to be speech
+ void audioInputEncountered(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech);
public:
typedef enum { ActivityStateIdle, ActivityStateReturnedFromIdle, ActivityStateActive } ActivityState;
diff --git a/src/mumble/AudioOutput.cpp b/src/mumble/AudioOutput.cpp
index 15a01600e..784a29245 100644
--- a/src/mumble/AudioOutput.cpp
+++ b/src/mumble/AudioOutput.cpp
@@ -12,7 +12,7 @@
#include "ChannelListener.h"
#include "Message.h"
#include "PacketDataStream.h"
-#include "Plugins.h"
+#include "PluginManager.h"
#include "ServerHandler.h"
#include "SpeechFlags.h"
#include "Timer.h"
@@ -395,20 +395,19 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
prioritySpeakerActive = true;
}
- if (!qlMix.isEmpty()) {
+ // If the audio backend uses a float-array we can sample and mix the audio sources directly into the output. Otherwise we'll have to
+ // use an intermediate buffer which we will convert to an array of shorts later
+ STACKVAR(float, fOutput, iChannels * frameCount);
+ float *output = (eSampleFormat == SampleFloat) ? reinterpret_cast<float *>(outbuff) : fOutput;
+ memset(output, 0, sizeof(float) * frameCount * iChannels);
+
+ if (! qlMix.isEmpty()) {
// There are audio sources available -> mix those sources together and feed them into the audio backend
STACKVAR(float, speaker, iChannels * 3);
STACKVAR(float, svol, iChannels);
- STACKVAR(float, fOutput, iChannels *frameCount);
-
- // If the audio backend uses a float-array we can sample and mix the audio sources directly into the output.
- // Otherwise we'll have to use an intermediate buffer which we will convert to an array of shorts later
- float *output = (eSampleFormat == SampleFloat) ? reinterpret_cast< float * >(outbuff) : fOutput;
bool validListener = false;
- memset(output, 0, sizeof(float) * frameCount * iChannels);
-
// Initialize recorder if recording is enabled
boost::shared_array< float > recbuff;
if (recorder) {
@@ -420,75 +419,56 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
for (unsigned int i = 0; i < iChannels; ++i)
svol[i] = mul * fSpeakerVolume[i];
- if (Global::get().s.bPositionalAudio && (iChannels > 1) && Global::get().p->fetch()
- && (Global::get().bPosTest || Global::get().p->fCameraPosition[0] != 0 || Global::get().p->fCameraPosition[1] != 0
- || Global::get().p->fCameraPosition[2] != 0)) {
+ if (Global::get().s.bPositionalAudio && (iChannels > 1) && Global::get().pluginManager->fetchPositionalData()) {
// Calculate the positional audio effects if it is enabled
- float front[3] = { Global::get().p->fCameraFront[0], Global::get().p->fCameraFront[1], Global::get().p->fCameraFront[2] };
- float top[3] = { Global::get().p->fCameraTop[0], Global::get().p->fCameraTop[1], Global::get().p->fCameraTop[2] };
-
- // Front vector is dominant; if it's zero we presume all is zero.
+ Vector3D cameraDir = Global::get().pluginManager->getPositionalData().getCameraDir();
- float flen = sqrtf(front[0] * front[0] + front[1] * front[1] + front[2] * front[2]);
+ Vector3D cameraAxis = Global::get().pluginManager->getPositionalData().getCameraAxis();
- if (flen > 0.0f) {
- front[0] *= (1.0f / flen);
- front[1] *= (1.0f / flen);
- front[2] *= (1.0f / flen);
+ // Direction vector is dominant; if it's zero we presume all is zero.
- float tlen = sqrtf(top[0] * top[0] + top[1] * top[1] + top[2] * top[2]);
+ if (!cameraDir.isZero()) {
+ cameraDir.normalize();
- if (tlen > 0.0f) {
- top[0] *= (1.0f / tlen);
- top[1] *= (1.0f / tlen);
- top[2] *= (1.0f / tlen);
+ if (!cameraAxis.isZero()) {
+ cameraAxis.normalize();
} else {
- top[0] = 0.0f;
- top[1] = 1.0f;
- top[2] = 0.0f;
+ cameraAxis = { 0.0f, 1.0f, 0.0f };
}
- const float dotproduct = front[0] * top[0] + front[1] * top[1] + front[2] * top[2];
+ const float dotproduct = cameraDir.dotProduct(cameraAxis);
const float error = std::abs(dotproduct);
if (error > 0.5f) {
// Not perpendicular by a large margin. Assume Y up and rotate 90 degrees.
float azimuth = 0.0f;
- if ((front[0] != 0.0f) || (front[2] != 0.0f))
- azimuth = atan2f(front[2], front[0]);
- float inclination = acosf(front[1]) - static_cast< float >(M_PI) / 2.0f;
+ if (cameraDir.x != 0.0f || cameraDir.z != 0.0f) {
+ azimuth = atan2f(cameraDir.z, cameraDir.x);
+ }
+
+ float inclination = acosf(cameraDir.y) - static_cast< float >(M_PI) / 2.0f;
- top[0] = sinf(inclination) * cosf(azimuth);
- top[1] = cosf(inclination);
- top[2] = sinf(inclination) * sinf(azimuth);
+ cameraAxis.x = sinf(inclination) * cosf(azimuth);
+ cameraAxis.y = cosf(inclination);
+ cameraAxis.z = sinf(inclination) * sinf(azimuth);
} else if (error > 0.01f) {
// Not perpendicular by a small margin. Find the nearest perpendicular vector.
+ cameraAxis = cameraAxis - cameraDir * dotproduct;
- top[0] -= front[0] * dotproduct;
- top[1] -= front[1] * dotproduct;
- top[2] -= front[2] * dotproduct;
-
- // normalize top again
- tlen = sqrtf(top[0] * top[0] + top[1] * top[1] + top[2] * top[2]);
- // tlen is guaranteed to be non-zero, otherwise error would have been larger than 0.5
- top[0] *= (1.0f / tlen);
- top[1] *= (1.0f / tlen);
- top[2] *= (1.0f / tlen);
+ // normalize axis again (the orthogonalized vector us guaranteed to be non-zero
+ // as the error (dotproduct) was only 0.5 (and not 1 in which case above operation
+ // would create the zero-vector).
+ cameraAxis.normalize();
}
} else {
- front[0] = 0.0f;
- front[1] = 0.0f;
- front[2] = 1.0f;
+ cameraDir = { 0.0f, 0.0f, 1.0f };
- top[0] = 0.0f;
- top[1] = 1.0f;
- top[2] = 0.0f;
+ cameraAxis = { 0.0f, 1.0f, 0.0f };
}
// Calculate right vector as front X top
- float right[3] = { top[1] * front[2] - top[2] * front[1], top[2] * front[0] - top[0] * front[2],
- top[0] * front[1] - top[1] * front[0] };
+ Vector3D right = cameraAxis.crossProduct(cameraDir);
/*
qWarning("Front: %f %f %f", front[0], front[1], front[2]);
@@ -497,26 +477,27 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
*/
// Rotate speakers to match orientation
for (unsigned int i = 0; i < iChannels; ++i) {
- speaker[3 * i + 0] =
- fSpeakers[3 * i + 0] * right[0] + fSpeakers[3 * i + 1] * top[0] + fSpeakers[3 * i + 2] * front[0];
- speaker[3 * i + 1] =
- fSpeakers[3 * i + 0] * right[1] + fSpeakers[3 * i + 1] * top[1] + fSpeakers[3 * i + 2] * front[1];
- speaker[3 * i + 2] =
- fSpeakers[3 * i + 0] * right[2] + fSpeakers[3 * i + 1] * top[2] + fSpeakers[3 * i + 2] * front[2];
+ speaker[3 * i + 0] = fSpeakers[3 * i + 0] * right.x + fSpeakers[3 * i + 1] * cameraAxis.x
+ + fSpeakers[3 * i + 2] * cameraDir.x;
+ speaker[3 * i + 1] = fSpeakers[3 * i + 0] * right.y + fSpeakers[3 * i + 1] * cameraAxis.y
+ + fSpeakers[3 * i + 2] * cameraDir.y;
+ speaker[3 * i + 2] = fSpeakers[3 * i + 0] * right.z + fSpeakers[3 * i + 1] * cameraAxis.z
+ + fSpeakers[3 * i + 2] * cameraDir.z;
}
validListener = true;
}
foreach (AudioOutputUser *aop, qlMix) {
// Iterate through all audio sources and mix them together into the output (or the intermediate array)
- const float *RESTRICT pfBuffer = aop->pfBuffer;
- float volumeAdjustment = 1;
+ float *RESTRICT pfBuffer = aop->pfBuffer;
+ float volumeAdjustment = 1;
// Check if the audio source is a user speaking (instead of a sample playback) and apply potential volume
// adjustments
AudioOutputSpeech *speech = qobject_cast< AudioOutputSpeech * >(aop);
+ const ClientUser *user = nullptr;
if (speech) {
- const ClientUser *user = speech->p;
+ user = speech->p;
volumeAdjustment *= user->getLocalVolumeAdjustments();
if (user->cChannel && ChannelListener::isListening(Global::get().uiSession, user->cChannel->iId)
@@ -534,6 +515,11 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
}
}
+ // As the events may cause the output PCM to change, the connection has to be direct in any case
+ const int channels = (speech && speech->bStereo) ? 2 : 1;
+ // If user != nullptr, then the current audio is considered speech
+ emit audioSourceFetched(pfBuffer, frameCount, channels, SAMPLE_RATE, static_cast< bool >(user), user);
+
// If recording is enabled add the current audio source to the recording buffer
if (recorder) {
if (speech) {
@@ -574,27 +560,33 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
#endif
// If positional audio is enabled, calculate the respective audio effect here
- float dir[3] = { aop->fPos[0] - Global::get().p->fCameraPosition[0], aop->fPos[1] - Global::get().p->fCameraPosition[1],
- aop->fPos[2] - Global::get().p->fCameraPosition[2] };
- float len = sqrtf(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
+ Position3D outputPos = { aop->fPos[0], aop->fPos[1], aop->fPos[2] };
+ Position3D ownPos = Global::get().pluginManager->getPositionalData().getCameraPos();
+
+ Vector3D connectionVec = outputPos - ownPos;
+ float len = connectionVec.norm();
+
if (len > 0.0f) {
- dir[0] /= len;
- dir[1] /= len;
- dir[2] /= len;
+ // Don't use normalize-func in order to save the re-computation of the vector's length
+ connectionVec.x /= len;
+ connectionVec.y /= len;
+ connectionVec.z /= len;
}
/*
qWarning("Voice pos: %f %f %f", aop->fPos[0], aop->fPos[1], aop->fPos[2]);
- qWarning("Voice dir: %f %f %f", dir[0], dir[1], dir[2]);
+ qWarning("Voice dir: %f %f %f", connectionVec.x, connectionVec.y, connectionVec.z);
*/
if (!aop->pfVolume) {
aop->pfVolume = new float[nchan];
for (unsigned int s = 0; s < nchan; ++s)
aop->pfVolume[s] = -1.0;
}
+
for (unsigned int s = 0; s < nchan; ++s) {
- const float dot = bSpeakerPositional[s] ? dir[0] * speaker[s * 3 + 0] + dir[1] * speaker[s * 3 + 1]
- + dir[2] * speaker[s * 3 + 2]
- : 1.0f;
+ const float dot = bSpeakerPositional[s]
+ ? connectionVec.x * speaker[s * 3 + 0] + connectionVec.y * speaker[s * 3 + 1]
+ + connectionVec.z * speaker[s * 3 + 2]
+ : 1.0f;
const float str = svol[s] * calcGain(dot, len) * volumeAdjustment;
float *RESTRICT o = output + s;
const float old = (aop->pfVolume[s] >= 0.0f) ? aop->pfVolume[s] : str;
@@ -642,7 +634,12 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
if (recorder && recorder->isInMixDownMode()) {
recorder->addBuffer(nullptr, recbuff, frameCount);
}
+ }
+
+ bool pluginModifiedAudio = false;
+ emit audioOutputAboutToPlay(output, frameCount, nchan, SAMPLE_RATE, &pluginModifiedAudio);
+ if (pluginModifiedAudio || (! qlMix.isEmpty())) {
// Clip the output audio
if (eSampleFormat == SampleFloat)
for (unsigned int i = 0; i < frameCount * iChannels; i++)
@@ -665,7 +662,7 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) {
#endif
// Return whether data has been written to the outbuff
- return (!qlMix.isEmpty());
+ return (pluginModifiedAudio || (! qlMix.isEmpty()));
}
bool AudioOutput::isAlive() const {
diff --git a/src/mumble/AudioOutput.h b/src/mumble/AudioOutput.h
index 299736b9d..3d5c5b6da 100644
--- a/src/mumble/AudioOutput.h
+++ b/src/mumble/AudioOutput.h
@@ -127,6 +127,25 @@ public:
static float calcGain(float dotproduct, float distance);
unsigned int getMixerFreq() const;
void setBufferSize(unsigned int bufferSize);
+
+signals:
+ /// Signal emitted whenever an audio source has been fetched
+ ///
+ /// @param outputPCM The fetched output PCM
+ /// @param sampleCount The amount of samples in the output
+ /// @param channelCount The amount of channels in the output
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether the fetched output is considered to be speech
+ /// @param A pointer to the user that this speech belongs to or nullptr if this isn't speech
+ void audioSourceFetched(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech, const ClientUser *user);
+ /// Signal emitted whenever an audio is about to be played to the user
+ ///
+ /// @param outputPCM The output PCM that is to be played
+ /// @param sampleCount The amount of samples in the output
+ /// @param channelCount The amount of channels in the output
+ /// @param sampleRate The used sample rate in Hz
+ /// @param modifiedAudio Pointer to bool if audio has been modified or not and should be played
+ void audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool *modifiedAudio);
};
#endif
diff --git a/src/mumble/CMakeLists.txt b/src/mumble/CMakeLists.txt
index d904be2b1..cf28c1dd5 100644
--- a/src/mumble/CMakeLists.txt
+++ b/src/mumble/CMakeLists.txt
@@ -35,6 +35,9 @@ option(qtspeech "Use Qt's text-to-speech system (part of the Qt Speech module) i
option(jackaudio "Build support for JackAudio." ON)
option(portaudio "Build support for PortAudio" ON)
+option(plugin-debug "Build Mumble with debug output for plugin developers." OFF)
+option(plugin-callback-debug "Build Mumble with debug output for plugin callbacks inside of Mumble." OFF)
+
if(WIN32)
option(asio "Build support for ASIO audio input." OFF)
option(wasapi "Build support for WASAPI." ON)
@@ -81,6 +84,8 @@ set(MUMBLE_SOURCES
"ACLEditor.cpp"
"ACLEditor.h"
"ACLEditor.ui"
+ "API_v_1_0_x.cpp"
+ "API.h"
"ApplicationPalette.h"
"AudioConfigDialog.cpp"
"AudioConfigDialog.h"
@@ -142,6 +147,8 @@ set(MUMBLE_SOURCES
"LCD.cpp"
"LCD.h"
"LCD.ui"
+ "LegacyPlugin.cpp"
+ "LegacyPlugin.h"
"ListenerLocalVolumeDialog.cpp"
"Log.cpp"
"Log.h"
@@ -162,9 +169,21 @@ set(MUMBLE_SOURCES
"NetworkConfig.ui"
"OpusCodec.cpp"
"OpusCodec.h"
- "Plugins.cpp"
- "Plugins.h"
- "Plugins.ui"
+ "PluginConfig.cpp"
+ "PluginConfig.h"
+ "PluginConfig.ui"
+ "Plugin.cpp"
+ "Plugin.h"
+ "PluginInstaller.cpp"
+ "PluginInstaller.h"
+ "PluginInstaller.ui"
+ "PluginManager.cpp"
+ "PluginManager.h"
+ "PluginUpdater.cpp"
+ "PluginUpdater.h"
+ "PluginUpdater.ui"
+ "PositionalData.cpp"
+ "PositionalData.h"
"PTTButtonWidget.cpp"
"PTTButtonWidget.h"
"PTTButtonWidget.ui"
@@ -347,8 +366,72 @@ target_include_directories(mumble
"widgets"
${SHARED_SOURCE_DIR}
"${3RDPARTY_DIR}/smallft"
+ "${PLUGINS_DIR}"
)
+find_pkg(Poco COMPONENTS Zip)
+
+if(TARGET Poco::Zip)
+ target_link_libraries(mumble
+ PRIVATE
+ Poco::Zip
+ )
+else()
+ message(STATUS "Regular Poco search failed - looking for Poco include dir manually...")
+
+ if(MINGW)
+ # These are the paths for our MXE environment
+ if(32_BIT)
+ set(POCO_INCLUDE_DIR_HINT "/usr/lib/mxe/usr/i686-w64-mingw32.static/include/")
+ else()
+ set(POCO_INCLUDE_DIR_HINT "/usr/lib/mxe/usr/x86_64-w64-mingw32.static/include/")
+ endif()
+ else()
+ set(POCO_INCLUDE_DIR_HINT "/usr/include")
+ endif()
+
+ find_path(POCO_INCLUDE_DIR "Poco/Poco.h" HINTS ${POCO_INCLUDE_DIR_HINT})
+
+ if(POCO_INCLUDE_DIR)
+ message(STATUS "Found Poco include dir at \"${POCO_INCLUDE_DIR}\"")
+ else()
+ message(FATAL_ERROR "Unable to locate Poco include directory")
+ endif()
+
+ find_library(POCO_LIB_FOUNDATION NAMES PocoFoundation PocoFoundationmd REQUIRED)
+ find_library(POCO_LIB_UTIL NAMES PocoUtil PocoUtilmd REQUIRED)
+ find_library(POCO_LIB_XML NAMES PocoXML PocoXMLmd REQUIRED)
+ find_library(POCO_LIB_ZIP NAMES PocoZip PocoZipmd REQUIRED)
+
+ if(POCO_LIB_ZIP)
+ message(STATUS "Found Poco Zip library at \"${POCO_LIB_ZIP}\"")
+ else()
+ message(FATAL_ERROR "Unable to find Poco Zip library")
+ endif()
+
+
+ # Now use the found include dir and libraries by linking it to the target
+ target_include_directories(mumble
+ PRIVATE
+ ${POCO_INCLUDE_DIR}
+ )
+
+ target_link_libraries(mumble
+ PRIVATE
+ ${POCO_LIB_ZIP}
+ ${POCO_LIB_XML}
+ ${POCO_LIB_UTIL}
+ ${POCO_LIB_FOUNDATION}
+ )
+
+ if(static)
+ target_compile_definitions(mumble
+ PUBLIC
+ POCO_STATIC
+ )
+ endif()
+endif()
+
find_pkg("SndFile;LibSndFile;sndfile" REQUIRED)
# Look for various targets as they are named differently on different platforms
@@ -1020,3 +1103,21 @@ if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
)
endif()
endif()
+
+if(plugin-debug)
+ target_compile_definitions(mumble PRIVATE "MUMBLE_PLUGIN_DEBUG")
+endif()
+
+if(plugin-callback-debug)
+ target_compile_definitions(mumble PRIVATE "MUMBLE_PLUGIN_CALLBACK_DEBUG")
+endif()
+
+if(UNIX)
+ if(${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
+ # On FreeBSD we need the util library for src/ProcessResolver.cpp to work
+ target_link_libraries(mumble PRIVATE util)
+ elseif(${CMAKE_SYSTEM_NAME} MATCHES ".*BSD")
+ # On any other BSD we need the kvm library for src/ProcessResolver.cpp to work
+ target_link_libraries(mumble PRIVATE kvm)
+ endif()
+endif()
diff --git a/src/mumble/ClientUser.cpp b/src/mumble/ClientUser.cpp
index 3e7c7152c..8a66cf496 100644
--- a/src/mumble/ClientUser.cpp
+++ b/src/mumble/ClientUser.cpp
@@ -7,6 +7,7 @@
#include "AudioOutput.h"
#include "Channel.h"
+#include "PluginManager.h"
#include "Global.h"
QHash< unsigned int, ClientUser * > ClientUser::c_qmUsers;
@@ -61,6 +62,9 @@ ClientUser *ClientUser::add(unsigned int uiSession, QObject *po) {
ClientUser *p = new ClientUser(po);
p->uiSession = uiSession;
c_qmUsers[uiSession] = p;
+
+ QObject::connect(p, &ClientUser::talkingStateChanged, Global::get().pluginManager, &PluginManager::on_userTalkingStateChanged);
+
return p;
}
diff --git a/src/mumble/Global.cpp b/src/mumble/Global.cpp
index acbbec7e1..b8c001ad1 100644
--- a/src/mumble/Global.cpp
+++ b/src/mumble/Global.cpp
@@ -73,7 +73,7 @@ static void migrateDataDir() {
Global::Global(const QString &qsConfigPath) {
mw = 0;
db = 0;
- p = 0;
+ pluginManager = 0;
nam = 0;
c = 0;
talkingUI = 0;
diff --git a/src/mumble/Global.h b/src/mumble/Global.h
index 5aee1abf5..d8748bb51 100644
--- a/src/mumble/Global.h
+++ b/src/mumble/Global.h
@@ -22,7 +22,7 @@ class AudioInput;
class AudioOutput;
class Database;
class Log;
-class Plugins;
+class PluginManager;
class QSettings;
class Overlay;
class LCD;
@@ -53,7 +53,8 @@ public:
*/
Database *db;
Log *l;
- Plugins *p;
+ /// A pointer to the PluginManager that is used in this session
+ PluginManager *pluginManager;
QSettings *qs;
#ifdef USE_OVERLAY
Overlay *o;
diff --git a/src/mumble/LegacyPlugin.cpp b/src/mumble/LegacyPlugin.cpp
new file mode 100644
index 000000000..420f3cf73
--- /dev/null
+++ b/src/mumble/LegacyPlugin.cpp
@@ -0,0 +1,267 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "LegacyPlugin.h"
+#include "MumblePlugin_v_1_0_x.h"
+
+#include <cstdlib>
+#include <wchar.h>
+#include <map>
+#include <string.h>
+#include <codecvt>
+#include <locale>
+
+#include <QRegularExpression>
+
+
+/// A regular expression used to extract the version from the legacy plugin's description
+static const QRegularExpression versionRegEx(QString::fromLatin1("(?:v)?(?:ersion)?[ \\t]*(\\d+)\\.(\\d+)(?:\\.(\\d+))?"), QRegularExpression::CaseInsensitiveOption);
+
+
+LegacyPlugin::LegacyPlugin(QString path, bool isBuiltIn, QObject *p)
+ : Plugin(path, isBuiltIn, p),
+ m_name(),
+ m_description(),
+ m_version(VERSION_UNKNOWN),
+ m_mumPlug(0),
+ m_mumPlug2(0),
+ m_mumPlugQt(0) {
+}
+
+LegacyPlugin::~LegacyPlugin() {
+}
+
+bool LegacyPlugin::doInitialize() {
+ if (Plugin::doInitialize()) {
+ // initialization seems to have succeeded so far
+ // This means that mumPlug is initialized
+
+ m_name = QString::fromStdWString(m_mumPlug->shortname);
+ // Although the MumblePlugin struct has a member called "description", the actual description seems to
+ // always only be returned by the longdesc function (The description member is actually just the name with some version
+ // info)
+ m_description = QString::fromStdWString(m_mumPlug->longdesc());
+ // The version field in the MumblePlugin2 struct is the positional-audio-plugin-API version and not the version
+ // of the plugin itself. This information is not provided for legacy plugins.
+ // Most of them however provide information about the version of the game they support. Thus we will try to parse the
+ // description and extract this version using it for the plugin's version as well.
+ // Some plugins have the version in the actual description field of the old API (see above comment why these aren't the same)
+ // so we will use a combination of both to search for the version. If multiple version(-like) strings are found, the last one
+ // will be used.
+ QString matchContent = m_description + QChar::Null + QString::fromStdWString(m_mumPlug->description);
+ QRegularExpressionMatchIterator matchIt = versionRegEx.globalMatch(matchContent);
+
+ // Only consider the last match
+ QRegularExpressionMatch match;
+ while (matchIt.hasNext()) {
+ match = matchIt.next();
+ }
+
+ if (match.hasMatch()) {
+ // Store version
+ m_version = { match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt() };
+ }
+
+ return true;
+ } else {
+ // initialization has failed
+ // pass on info about failed init
+ return false;
+ }
+}
+
+void LegacyPlugin::resolveFunctionPointers() {
+ // We don't set any functions inside the apiFnc struct variable in order for the default
+ // implementations in the Plugin class to mimic empty default implementations for all functions
+ // not explicitly overwritten by this class
+
+ if (isValid()) {
+ // The corresponding library was loaded -> try to locate all API functions of the legacy plugin's spec
+ // (for positional audio) and set defaults for the other ones in order to maintain compatibility with
+ // the new plugin system
+
+ QWriteLocker lock(&m_pluginLock);
+
+ mumblePluginFunc pluginFunc = reinterpret_cast<mumblePluginFunc>(m_lib.resolve("getMumblePlugin"));
+ mumblePlugin2Func plugin2Func = reinterpret_cast<mumblePlugin2Func>(m_lib.resolve("getMumblePlugin2"));
+ mumblePluginQtFunc pluginQtFunc = reinterpret_cast<mumblePluginQtFunc>(m_lib.resolve("getMumblePluginQt"));
+
+ if (pluginFunc) {
+ m_mumPlug = pluginFunc();
+ }
+ if (plugin2Func) {
+ m_mumPlug2 = plugin2Func();
+ }
+ if (pluginQtFunc) {
+ m_mumPlugQt = pluginQtFunc();
+ }
+
+ // A legacy plugin is valid as long as there is a function to get the MumblePlugin struct from it
+ // and the plugin has been compiled by the same compiler as this client (determined by the plugin's
+ // "magic") and it isn't retracted
+ bool suitableMagic = m_mumPlug && m_mumPlug->magic == MUMBLE_PLUGIN_MAGIC;
+ bool retracted = m_mumPlug && m_mumPlug->shortname == L"Retracted";
+ m_pluginIsValid = pluginFunc && suitableMagic && !retracted;
+
+#ifdef MUMBLE_PLUGIN_DEBUG
+ if (!m_pluginIsValid) {
+ if (!pluginFunc) {
+ qDebug("Plugin \"%s\" is missing the getMumblePlugin() function", qPrintable(m_pluginPath));
+ } else if (!suitableMagic) {
+ qDebug("Plugin \"%s\" was compiled with a different compiler (magic differs)", qPrintable(m_pluginPath));
+ } else {
+ qDebug("Plugin \"%s\" is retracted", qPrintable(m_pluginPath));
+ }
+ }
+#endif
+ }
+}
+
+mumble_error_t LegacyPlugin::init() {
+ {
+ QWriteLocker lock(&m_pluginLock);
+
+ m_pluginIsLoaded = true;
+ }
+
+ // No-op as legacy plugins never have anything to initialize
+ // The only init function they care about is the one that inits positional audio
+ return STATUS_OK;
+}
+
+QString LegacyPlugin::getName() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ if (!m_name.isEmpty()) {
+ return m_name;
+ } else {
+ return QString::fromLatin1("<Unknown Legacy Plugin>");
+ }
+}
+
+QString LegacyPlugin::getDescription() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ if (!m_description.isEmpty()) {
+ return m_description;
+ } else {
+ return QString::fromLatin1("<No description provided by the legacy plugin>");
+ }
+}
+
+bool LegacyPlugin::showAboutDialog(QWidget *parent) const {
+ if (m_mumPlugQt && m_mumPlugQt->about) {
+ m_mumPlugQt->about(parent);
+
+ return true;
+ }
+ if (m_mumPlug->about) {
+ // the original implementation in Mumble would pass nullptr to the about-function in the mumPlug struct
+ // so we'll mimic that behaviour for compatibility
+ m_mumPlug->about(nullptr);
+
+ return true;
+ }
+
+ return false;
+}
+
+bool LegacyPlugin::showConfigDialog(QWidget *parent) const {
+ if (m_mumPlugQt && m_mumPlugQt->config) {
+ m_mumPlugQt->config(parent);
+
+ return true;
+ }
+ if (m_mumPlug->config) {
+ // the original implementation in Mumble would pass nullptr to the about-function in the mumPlug struct
+ // so we'll mimic that behaviour for compatibility
+ m_mumPlug->config(nullptr);
+
+ return true;
+ }
+
+ return false;
+}
+
+uint8_t LegacyPlugin::initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) {
+ int retCode;
+
+ if (m_mumPlug2) {
+ // Create and populate a multimap holding the names and PIDs to pass to the tryLock-function
+ std::multimap<std::wstring, unsigned long long int> pidMap;
+
+ for (size_t i=0; i<programCount; i++) {
+ std::string currentName = programNames[i];
+ std::wstring currentNameWstr = std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(currentName);
+
+ pidMap.insert(std::pair<std::wstring, unsigned long long int>(currentNameWstr, programPIDs[i]));
+ }
+
+ retCode = m_mumPlug2->trylock(pidMap);
+ } else {
+ // The default MumblePlugin doesn't take the name and PID arguments
+ retCode = m_mumPlug->trylock();
+ }
+
+ // ensure that only expected return codes are being returned from this function
+ // the legacy plugins return 1 on successfull locking and 0 on failure
+ if (retCode) {
+ QWriteLocker wLock(&m_pluginLock);
+
+ m_positionalDataIsActive = true;
+
+ return PDEC_OK;
+ } else {
+ // legacy plugins don't have the concept of indicating a permanent error
+ // so we'll return a temporary error for them
+ return PDEC_ERROR_TEMP;
+ }
+}
+
+bool LegacyPlugin::fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir,
+ Vector3D& cameraAxis, QString& context, QString& identity) const {
+ std::wstring identityWstr;
+ std::string contextStr;
+
+ int retCode = m_mumPlug->fetch(static_cast<float*>(avatarPos), static_cast<float*>(avatarDir), static_cast<float*>(avatarAxis),
+ static_cast<float*>(cameraPos), static_cast<float*>(cameraDir), static_cast<float*>(cameraAxis), contextStr, identityWstr);
+
+ context = QString::fromStdString(contextStr);
+ identity = QString::fromStdWString(identityWstr);
+
+ // The fetch-function should return if it is "still locked on" meaning that it can continue providing
+ // positional audio
+ return retCode == 1;
+}
+
+void LegacyPlugin::shutdownPositionalData() {
+ QWriteLocker lock(&m_pluginLock);
+
+ m_positionalDataIsActive = false;
+
+ m_mumPlug->unlock();
+}
+
+uint32_t LegacyPlugin::getFeatures() const {
+ return FEATURE_POSITIONAL;
+}
+
+mumble_version_t LegacyPlugin::getVersion() const {
+ return m_version;
+}
+
+bool LegacyPlugin::providesAboutDialog() const {
+ return m_mumPlug->about || (m_mumPlugQt && m_mumPlugQt->about);
+}
+
+bool LegacyPlugin::providesConfigDialog() const {
+ return m_mumPlug->config || (m_mumPlugQt && m_mumPlugQt->config);
+}
+
+mumble_version_t LegacyPlugin::getAPIVersion() const {
+ // Legacy plugins are always on most recent API as they don't use it in any case -> no need to perform
+ // backwards compatibility stuff
+ return MUMBLE_PLUGIN_API_VERSION;
+}
diff --git a/src/mumble/LegacyPlugin.h b/src/mumble/LegacyPlugin.h
new file mode 100644
index 000000000..250b624a0
--- /dev/null
+++ b/src/mumble/LegacyPlugin.h
@@ -0,0 +1,82 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_LEGACY_PLUGIN_H_
+#define MUMBLE_MUMBLE_LEGACY_PLUGIN_H_
+
+#include "Plugin.h"
+
+#include <QtCore/QString>
+
+#include <string>
+#include <memory>
+
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "mumble_legacy_plugin.h"
+
+class LegacyPlugin;
+
+/// Typedef for a LegacyPlugin pointer
+typedef std::shared_ptr<LegacyPlugin> legacy_plugin_ptr_t;
+/// Typedef for a const LegacyPlugin pointer
+typedef std::shared_ptr<const LegacyPlugin> const_legacy_plugin_ptr_t;
+
+
+/// This class is meant for compatibility for old Mumble "plugins" that stem from before the plugin framework has been
+/// introduced. Thus the "plugins" represented by this class are for positional data gathering only.
+class LegacyPlugin : public Plugin {
+ friend class Plugin; // needed in order for Plugin::createNew to access LegacyPlugin::doInitialize()
+ private:
+ Q_OBJECT
+ Q_DISABLE_COPY(LegacyPlugin)
+
+ protected:
+ /// The name of the "plugin"
+ QString m_name;
+ /// The description of the "plugin"
+ QString m_description;
+ /// The Version of the "plugin"
+ mumble_version_t m_version;
+ /// A pointer to the PluginStruct in its initial version. After initialization this
+ /// field is effectively const and therefore it is not needed to protect read-access by a lock.
+ MumblePlugin *m_mumPlug;
+ /// A pointer to the PluginStruct in its second, enhanced version. After initialization this
+ /// field is effectively const and therefore it is not needed to protect read-access by a lock.
+ MumblePlugin2 *m_mumPlug2;
+ /// A pointer to the PluginStruct that encorporates Qt functionality. After initialization this
+ /// field is effectively const and therefore it is not needed to protect read-access by a lock.
+ MumblePluginQt *m_mumPlugQt;
+
+ virtual void resolveFunctionPointers() override;
+ virtual bool doInitialize() override;
+
+ LegacyPlugin(QString path, bool isBuiltIn = false, QObject *p = 0);
+
+ virtual bool showAboutDialog(QWidget *parent) const override;
+ virtual bool showConfigDialog(QWidget *parent) const override;
+ virtual uint8_t initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) override;
+ virtual bool fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir,
+ Vector3D& cameraAxis, QString& context, QString& identity) const override;
+ virtual void shutdownPositionalData() override;
+ public:
+ virtual ~LegacyPlugin() override;
+
+ virtual mumble_error_t init() override;
+
+ // functions for direct plugin-interaction
+ virtual QString getName() const override;
+
+ virtual QString getDescription() const override;
+ virtual uint32_t getFeatures() const override;
+ virtual mumble_version_t getAPIVersion() const override;
+
+ virtual mumble_version_t getVersion() const override;
+
+ // functions for checking which underlying plugin functions are implemented
+ virtual bool providesAboutDialog() const override;
+ virtual bool providesConfigDialog() const override;
+};
+
+#endif
diff --git a/src/mumble/Log.cpp b/src/mumble/Log.cpp
index d4c011197..5a2b14edc 100644
--- a/src/mumble/Log.cpp
+++ b/src/mumble/Log.cpp
@@ -334,6 +334,8 @@ QVector< LogMessage > Log::qvDeferredLogs;
Log::Log(QObject *p) : QObject(p) {
+ qRegisterMetaType<Log::MsgType>();
+
#ifndef USE_NO_TTS
tts = new TextToSpeech(this);
tts->setVolume(Global::get().s.iTTSVolume);
@@ -374,7 +376,8 @@ const Log::MsgType Log::msgOrder[] = { DebugInfo,
ChannelLeaveDisconnect,
PermissionDenied,
TextMessage,
- PrivateTextMessage };
+ PrivateTextMessage,
+ PluginMessage };
const char *Log::msgNames[] = { QT_TRANSLATE_NOOP("Log", "Debug"),
QT_TRANSLATE_NOOP("Log", "Critical"),
@@ -406,7 +409,8 @@ const char *Log::msgNames[] = { QT_TRANSLATE_NOOP("Log", "Debug"),
QT_TRANSLATE_NOOP("Log", "User left channel and disconnected"),
QT_TRANSLATE_NOOP("Log", "Private text message"),
QT_TRANSLATE_NOOP("Log", "User started listening to channel"),
- QT_TRANSLATE_NOOP("Log", "User stopped listening to channel") };
+ QT_TRANSLATE_NOOP("Log", "User stopped listening to channel"),
+ QT_TRANSLATE_NOOP("Log", "Plugin message") };
QString Log::msgName(MsgType t) const {
return tr(msgNames[t]);
diff --git a/src/mumble/Log.h b/src/mumble/Log.h
index 6958f2be9..d584f2a49 100644
--- a/src/mumble/Log.h
+++ b/src/mumble/Log.h
@@ -90,8 +90,10 @@ public:
ChannelLeaveDisconnect,
PrivateTextMessage,
ChannelListeningAdd,
- ChannelListeningRemove
+ ChannelListeningRemove,
+ PluginMessage
};
+
enum LogColorType { Time, Server, Privilege, Source, Target };
static const MsgType firstMsgType = DebugInfo;
static const MsgType lastMsgType = ChannelListeningRemove;
@@ -134,7 +136,9 @@ public:
static void logOrDefer(Log::MsgType mt, const QString &console, const QString &terse = QString(),
bool ownMessage = false, const QString &overrideTTS = QString(), bool ignoreTTS = false);
public slots:
- void log(MsgType mt, const QString &console, const QString &terse = QString(), bool ownMessage = false,
+ // We have to explicitly use Log::MsgType and not only MsgType in order to be able to use QMetaObject::invokeMethod
+ // with this function.
+ void log(Log::MsgType mt, const QString &console, const QString &terse = QString(), bool ownMessage = false,
const QString &overrideTTS = QString(), bool ignoreTTS = false);
/// Logs LogMessages that have been deferred so far
void processDeferredLogs();
@@ -172,4 +176,6 @@ public:
LogDocumentResourceAddedEvent();
};
+Q_DECLARE_METATYPE(Log::MsgType);
+
#endif
diff --git a/src/mumble/MainWindow.cpp b/src/mumble/MainWindow.cpp
index e7ae820d1..f2718f0a3 100644
--- a/src/mumble/MainWindow.cpp
+++ b/src/mumble/MainWindow.cpp
@@ -33,8 +33,8 @@
#include "ChannelListener.h"
#include "ListenerLocalVolumeDialog.h"
#include "Markdown.h"
+#include "PluginManager.h"
#include "PTTButtonWidget.h"
-#include "Plugins.h"
#include "RichTextEditor.h"
#include "SSLCipherInfo.h"
#include "Screen.h"
@@ -185,6 +185,8 @@ MainWindow::MainWindow(QWidget *p) : QMainWindow(p) {
setOnTop(Global::get().s.aotbAlwaysOnTop == Settings::OnTopAlways
|| (Global::get().s.bMinimalView && Global::get().s.aotbAlwaysOnTop == Settings::OnTopInMinimal)
|| (!Global::get().s.bMinimalView && Global::get().s.aotbAlwaysOnTop == Settings::OnTopInNormal));
+
+ QObject::connect(this, &MainWindow::serverSynchronized, Global::get().pluginManager, &PluginManager::on_serverSynchronized);
}
void MainWindow::createActions() {
@@ -318,6 +320,13 @@ void MainWindow::setupGui() {
QObject::connect(&ChannelListener::get(), &ChannelListener::localVolumeAdjustmentsChanged, pmModel,
&UserModel::on_channelListenerLocalVolumeAdjustmentChanged);
+ // connect slots to PluginManager
+ QObject::connect(pmModel, &UserModel::userAdded, Global::get().pluginManager, &PluginManager::on_userAdded);
+ QObject::connect(pmModel, &UserModel::userRemoved, Global::get().pluginManager, &PluginManager::on_userRemoved);
+ QObject::connect(pmModel, &UserModel::channelAdded, Global::get().pluginManager, &PluginManager::on_channelAdded);
+ QObject::connect(pmModel, &UserModel::channelRemoved, Global::get().pluginManager, &PluginManager::on_channelRemoved);
+ QObject::connect(pmModel, &UserModel::channelRenamed, Global::get().pluginManager, &PluginManager::on_channelRenamed);
+
qaAudioMute->setChecked(Global::get().s.bMute);
qaAudioDeaf->setChecked(Global::get().s.bDeaf);
#ifdef USE_NO_TTS
@@ -902,6 +911,16 @@ static void recreateServerHandler() {
SLOT(resolverError(QAbstractSocket::SocketError, QString)));
QObject::connect(sh.get(), &ServerHandler::disconnected, Global::get().talkingUI, &TalkingUI::on_serverDisconnected);
+
+ // We have to use direct connections for these here as the PluginManager must be able to access the connection's ID
+ // and in order for that to be possible the (dis)connection process must not proceed in the background.
+ Global::get().pluginManager->connect(sh.get(), &ServerHandler::connected, Global::get().pluginManager,
+ &PluginManager::on_serverConnected, Qt::DirectConnection);
+ // We connect the plugin manager to "aboutToDisconnect" instead of "disconnect" in order for the slot to be
+ // guaranteed to be completed *before* the acutal disconnect logic (e.g. MainWindow::serverDisconnected) kicks in.
+ // In order for that to work it is ESSENTIAL to use a DIRECT CONNECTION!
+ Global::get().pluginManager->connect(sh.get(), &ServerHandler::aboutToDisconnect, Global::get().pluginManager,
+ &PluginManager::on_serverDisconnected, Qt::DirectConnection);
}
void MainWindow::openUrl(const QUrl &url) {
@@ -2491,6 +2510,12 @@ void MainWindow::on_qaAudioMute_triggered() {
updateTrayIcon();
}
+void MainWindow::setAudioMute(bool mute) {
+ // Pretend the user pushed the button manually
+ qaAudioMute->setChecked(mute);
+ qaAudioMute->triggered(mute);
+}
+
void MainWindow::on_qaAudioDeaf_triggered() {
if (Global::get().bInAudioWizard) {
qaAudioDeaf->setChecked(!qaAudioDeaf->isChecked());
@@ -2503,11 +2528,13 @@ void MainWindow::on_qaAudioDeaf_triggered() {
on_qaAudioMute_triggered();
return;
}
+
AudioInputPtr ai = Global::get().ai;
if (ai)
ai->tIdle.restart();
Global::get().s.bDeaf = qaAudioDeaf->isChecked();
+
if (Global::get().s.bDeaf && !Global::get().s.bMute) {
bAutoUnmute = true;
Global::get().s.bMute = true;
@@ -2527,6 +2554,12 @@ void MainWindow::on_qaAudioDeaf_triggered() {
updateTrayIcon();
}
+void MainWindow::setAudioDeaf(bool deaf) {
+ // Pretend the user pushed the button manually
+ qaAudioDeaf->setChecked(deaf);
+ qaAudioDeaf->triggered(deaf);
+}
+
void MainWindow::on_qaRecording_triggered() {
if (voiceRecorderDialog) {
voiceRecorderDialog->show();
@@ -2549,7 +2582,7 @@ void MainWindow::on_qaAudioStats_triggered() {
}
void MainWindow::on_qaAudioUnlink_triggered() {
- Global::get().p->bUnlink = true;
+ Global::get().pluginManager->unlinkPositionalData();
}
void MainWindow::on_qaConfigDialog_triggered() {
diff --git a/src/mumble/MainWindow.h b/src/mumble/MainWindow.h
index f600ede14..20c8a3000 100644
--- a/src/mumble/MainWindow.h
+++ b/src/mumble/MainWindow.h
@@ -311,6 +311,14 @@ public slots:
/// Updates the user's image directory to the given path (any included
/// filename is discarded).
void updateImagePath(QString filepath) const;
+ /// Sets the local user's mute state
+ ///
+ /// @param mute Whether to mute the user
+ void setAudioMute(bool mute);
+ /// Sets the local user's deaf state
+ ///
+ /// @param deaf Whether to deafen the user
+ void setAudioDeaf(bool deaf);
signals:
/// Signal emitted when the server and the client have finished
/// synchronizing (after a new connection).
diff --git a/src/mumble/ManualPlugin.cpp b/src/mumble/ManualPlugin.cpp
index 981273c5f..e4ea84336 100644
--- a/src/mumble/ManualPlugin.cpp
+++ b/src/mumble/ManualPlugin.cpp
@@ -9,13 +9,15 @@
#include "ManualPlugin.h"
#include "ui_ManualPlugin.h"
+#include "Global.h"
+
#include <QPointer>
#include <float.h>
#include <cmath>
-#include "../../plugins/mumble_plugin.h"
-#include "Global.h"
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "../../plugins/mumble_legacy_plugin.h"
static QPointer< Manual > mDlg = nullptr;
static bool bLinkable = false;
@@ -43,7 +45,7 @@ Manual::Manual(QWidget *p) : QDialog(p) {
qgvPosition->viewport()->installEventFilter(this);
qgvPosition->scale(1.0f, 1.0f);
- qgsScene = new QGraphicsScene(QRectF(-5.0f, -5.0f, 10.0f, 10.0f), this);
+ m_qgsScene = new QGraphicsScene(QRectF(-5.0f, -5.0f, 10.0f, 10.0f), this);
const float indicatorDiameter = 4.0f;
QPainterPath indicator;
@@ -53,9 +55,9 @@ Manual::Manual(QWidget *p) : QDialog(p) {
indicator.moveTo(0, indicatorDiameter / 2);
indicator.lineTo(0, indicatorDiameter);
- qgiPosition = qgsScene->addPath(indicator);
+ m_qgiPosition = m_qgsScene->addPath(indicator);
- qgvPosition->setScene(qgsScene);
+ qgvPosition->setScene(m_qgsScene);
qgvPosition->fitInView(-5.0f, -5.0f, 10.0f, 10.0f, Qt::KeepAspectRatio);
qdsbX->setRange(-FLT_MAX, FLT_MAX);
@@ -101,7 +103,7 @@ bool Manual::eventFilter(QObject *obj, QEvent *evt) {
QPointF qpf = qgvPosition->mapToScene(qme->pos());
qdsbX->setValue(qpf.x());
qdsbZ->setValue(-qpf.y());
- qgiPosition->setPos(qpf);
+ m_qgiPosition->setPos(qpf);
}
}
}
@@ -134,8 +136,8 @@ void Manual::on_qpbActivated_clicked(bool b) {
}
void Manual::on_qdsbX_valueChanged(double d) {
- my.avatar_pos[0] = my.camera_pos[0] = static_cast< float >(d);
- qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
+ my.avatar_pos[0] = my.camera_pos[0] = static_cast<float>(d);
+ m_qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
}
void Manual::on_qdsbY_valueChanged(double d) {
@@ -143,8 +145,8 @@ void Manual::on_qdsbY_valueChanged(double d) {
}
void Manual::on_qdsbZ_valueChanged(double d) {
- my.avatar_pos[2] = my.camera_pos[2] = static_cast< float >(d);
- qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
+ my.avatar_pos[2] = my.camera_pos[2] = static_cast<float>(d);
+ m_qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]);
}
void Manual::on_qsbAzimuth_valueChanged(int i) {
@@ -262,7 +264,7 @@ void Manual::on_speakerPositionUpdate(QHash< unsigned int, Position2D > position
remainingIt.next();
const float speakerRadius = 1.2;
- QGraphicsItem *speakerItem = qgsScene->addEllipse(-speakerRadius, -speakerRadius, 2 * speakerRadius,
+ QGraphicsItem *speakerItem = m_qgsScene->addEllipse(-speakerRadius, -speakerRadius, 2 * speakerRadius,
2 * speakerRadius, QPen(), QBrush(Qt::red));
Position2D pos = remainingIt.value();
@@ -317,7 +319,7 @@ void Manual::updateTopAndFront(int azimuth, int elevation) {
iAzimuth = azimuth;
iElevation = elevation;
- qgiPosition->setRotation(azimuth);
+ m_qgiPosition->setRotation(azimuth);
double azim = azimuth * M_PI / 180.;
double elev = elevation * M_PI / 180.;
@@ -415,3 +417,16 @@ MumblePlugin *ManualPlugin_getMumblePlugin() {
MumblePluginQt *ManualPlugin_getMumblePluginQt() {
return &manualqt;
}
+
+
+/////////// Implementation of the ManualPlugin class //////////////
+ManualPlugin::ManualPlugin(QObject *p) : LegacyPlugin(QString::fromLatin1("manual.builtin"), true, p) {
+}
+
+ManualPlugin::~ManualPlugin() {
+}
+
+void ManualPlugin::resolveFunctionPointers() {
+ m_mumPlug = &manual;
+ m_mumPlugQt = &manualqt;
+}
diff --git a/src/mumble/ManualPlugin.h b/src/mumble/ManualPlugin.h
index fa76efbc8..dbeee0d31 100644
--- a/src/mumble/ManualPlugin.h
+++ b/src/mumble/ManualPlugin.h
@@ -12,8 +12,7 @@
#include <QtWidgets/QGraphicsScene>
#include "ui_ManualPlugin.h"
-
-#include "../../plugins/mumble_plugin.h"
+#include "LegacyPlugin.h"
#include <atomic>
#include <chrono>
@@ -67,8 +66,8 @@ public slots:
void on_updateStaleSpeakers();
protected:
- QGraphicsScene *qgsScene;
- QGraphicsItem *qgiPosition;
+ QGraphicsScene *m_qgsScene;
+ QGraphicsItem *m_qgiPosition;
std::atomic< bool > updateLoopRunning;
@@ -83,4 +82,20 @@ protected:
MumblePlugin *ManualPlugin_getMumblePlugin();
MumblePluginQt *ManualPlugin_getMumblePluginQt();
+
+/// A built-in "plugin" for positional data gatherig allowing for manually placing the "players" in a UI
+class ManualPlugin : public LegacyPlugin {
+ friend class Plugin; // needed in order for Plugin::createNew to access LegacyPlugin::doInitialize()
+ private:
+ Q_OBJECT
+ Q_DISABLE_COPY(ManualPlugin)
+
+ protected:
+ virtual void resolveFunctionPointers() Q_DECL_OVERRIDE;
+ ManualPlugin(QObject *p = nullptr);
+
+ public:
+ virtual ~ManualPlugin() Q_DECL_OVERRIDE;
+};
+
#endif
diff --git a/src/mumble/Messages.cpp b/src/mumble/Messages.cpp
index 62887c34f..729085f5e 100644
--- a/src/mumble/Messages.cpp
+++ b/src/mumble/Messages.cpp
@@ -24,7 +24,6 @@
# include "Overlay.h"
#endif
#include "ChannelListener.h"
-#include "Plugins.h"
#include "ServerHandler.h"
#include "TalkingUI.h"
#include "User.h"
@@ -35,6 +34,7 @@
#include "VersionCheck.h"
#include "ViewCert.h"
#include "crypto/CryptState.h"
+#include "PluginManager.h"
#include "Global.h"
#include <QTextDocumentFragment>
@@ -1303,6 +1303,25 @@ void MainWindow::msgSuggestConfig(const MumbleProto::SuggestConfig &msg) {
}
}
+void MainWindow::msgPluginDataTransmission(const MumbleProto::PluginDataTransmission &msg) {
+ // Another client's plugin has sent us some data. Verify the necessary parts are there and delegate it to the
+ // PluginManager
+
+ if (!msg.has_sendersession() || !msg.has_data() || !msg.has_dataid()) {
+ // if the message contains no sender session, no data or no ID for the data, it is of no use to us and we discard it
+ return;
+ }
+
+ const ClientUser *sender = ClientUser::get(msg.sendersession());
+ const std::string &data = msg.data();
+
+ if (sender) {
+ static_assert(sizeof(unsigned char) == sizeof(uint8_t), "Unsigned char does not have expected 8bit size");
+ // As long as above assertion is true, we are only casting away the sign, which is fine
+ Global::get().pluginManager->on_receiveData(sender, reinterpret_cast< const uint8_t * >(data.c_str()), data.size(), msg.dataid().c_str());
+ }
+}
+
#undef ACTOR_INIT
#undef VICTIM_INIT
#undef SELF_INIT
diff --git a/src/mumble/NetworkConfig.cpp b/src/mumble/NetworkConfig.cpp
index 82cd5c9f4..6b75bd644 100644
--- a/src/mumble/NetworkConfig.cpp
+++ b/src/mumble/NetworkConfig.cpp
@@ -28,6 +28,7 @@ static ConfigRegistrar registrarNetworkConfig(1300, NetworkConfigNew);
NetworkConfig::NetworkConfig(Settings &st) : ConfigWidget(st) {
setupUi(this);
+
qcbType->setAccessibleName(tr("Type"));
qleHostname->setAccessibleName(tr("Hostname"));
qlePort->setAccessibleName(tr("Port"));
@@ -72,7 +73,8 @@ void NetworkConfig::load(const Settings &r) {
const QSignalBlocker blocker(qcbAutoUpdate);
loadCheckBox(qcbAutoUpdate, r.bUpdateCheck);
- loadCheckBox(qcbPluginUpdate, r.bPluginCheck);
+ loadCheckBox(qcbPluginUpdateCheck, r.bPluginCheck);
+ loadCheckBox(qcbPluginAutoUpdate, r.bPluginAutoUpdate);
loadCheckBox(qcbUsage, r.bUsage);
}
@@ -91,9 +93,10 @@ void NetworkConfig::save() const {
s.qsProxyUsername = qleUsername->text();
s.qsProxyPassword = qlePassword->text();
- s.bUpdateCheck = qcbAutoUpdate->isChecked();
- s.bPluginCheck = qcbPluginUpdate->isChecked();
- s.bUsage = qcbUsage->isChecked();
+ s.bUpdateCheck = qcbAutoUpdate->isChecked();
+ s.bPluginCheck = qcbPluginUpdateCheck->isChecked();
+ s.bPluginAutoUpdate = qcbPluginAutoUpdate->isChecked();
+ s.bUsage = qcbUsage->isChecked();
}
static QNetworkProxy::ProxyType local_to_qt_proxy(Settings::ProxyType pt) {
diff --git a/src/mumble/NetworkConfig.ui b/src/mumble/NetworkConfig.ui
index f9c291288..ef783a987 100644
--- a/src/mumble/NetworkConfig.ui
+++ b/src/mumble/NetworkConfig.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>576</width>
- <height>572</height>
+ <height>584</height>
</rect>
</property>
<property name="windowTitle">
@@ -315,7 +315,7 @@ Prevents the client from sending potentially identifying information about the o
</widget>
</item>
<item>
- <widget class="QCheckBox" name="qcbPluginUpdate">
+ <widget class="QCheckBox" name="qcbPluginUpdateCheck">
<property name="toolTip">
<string>Check for new releases of plugins automatically.</string>
</property>
@@ -323,7 +323,14 @@ Prevents the client from sending potentially identifying information about the o
<string>This will check for new releases of plugins every time you start the program, and download them automatically.</string>
</property>
<property name="text">
- <string>Download plugin and overlay updates on startup</string>
+ <string>Check for plugin updates on startup</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="qcbPluginAutoUpdate">
+ <property name="text">
+ <string>Automatically download and install plugin updates</string>
</property>
</widget>
</item>
diff --git a/src/mumble/Plugin.cpp b/src/mumble/Plugin.cpp
new file mode 100644
index 000000000..8317c584f
--- /dev/null
+++ b/src/mumble/Plugin.cpp
@@ -0,0 +1,694 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "Plugin.h"
+#include "Version.h"
+#include "API.h"
+
+#include <QWriteLocker>
+#include <QMutexLocker>
+
+#include <cstring>
+
+
+// initialize the static ID counter
+plugin_id_t Plugin::s_nextID = 1;
+QMutex Plugin::s_idLock(QMutex::NonRecursive);
+
+void assertPluginLoaded(const Plugin* plugin) {
+ // don't throw and exception in release build
+ if (!plugin->isLoaded()) {
+#ifdef QT_DEBUG
+ throw std::runtime_error("Attempting to access plugin but it is not loaded!");
+#else
+ qWarning("Plugin assertion failed: Assumed plugin with ID %d to be loaded but it wasn't!", plugin->getID());
+#endif
+ }
+}
+
+Plugin::Plugin(QString path, bool isBuiltIn, QObject *p)
+ : QObject(p),
+ m_lib(path),
+ m_pluginPath(path),
+ m_pluginIsLoaded(false),
+ m_pluginLock(QReadWriteLock::NonRecursive),
+ m_pluginFnc(),
+ m_isBuiltIn(isBuiltIn),
+ m_positionalDataIsEnabled(true),
+ m_positionalDataIsActive(false),
+ m_mayMonitorKeyboard(false) {
+ // See if the plugin is loadable in the first place unless it is a built-in plugin
+ m_pluginIsValid = isBuiltIn || m_lib.load();
+
+ if (!m_pluginIsValid) {
+ // throw an exception to indicate that the plugin isn't valid
+ throw PluginError("Unable to load the specified library");
+ }
+
+ // aquire id-lock in order to assign an ID to this plugin
+ QMutexLocker lock(&Plugin::s_idLock);
+ m_pluginID = Plugin::s_nextID;
+ Plugin::s_nextID++;
+}
+
+Plugin::~Plugin() {
+ if (isLoaded()) {
+ shutdown();
+ }
+ if (m_lib.isLoaded()) {
+ m_lib.unload();
+ }
+}
+
+QString Plugin::extractWrappedString(MumbleStringWrapper wrapper) const {
+ QString wrappedString = QString::fromUtf8(wrapper.data, wrapper.size);
+
+ if (wrapper.needsReleasing) {
+ releaseResource(static_cast<const void *>(wrapper.data));
+ }
+
+ return wrappedString;
+}
+
+bool Plugin::doInitialize() {
+ resolveFunctionPointers();
+
+ return m_pluginIsValid;
+}
+
+void Plugin::resolveFunctionPointers() {
+ if (isValid()) {
+ // The corresponding library was loaded -> try to locate all API functions and provide defaults for
+ // the missing ones
+
+ QWriteLocker lock(&m_pluginLock);
+
+ // resolve the mandatory functions first
+ m_pluginFnc.init = reinterpret_cast<decltype(MumblePluginFunctions::init)>(m_lib.resolve("mumble_init"));
+ m_pluginFnc.shutdown = reinterpret_cast<decltype(MumblePluginFunctions::shutdown)>(m_lib.resolve("mumble_shutdown"));
+ m_pluginFnc.getName = reinterpret_cast<decltype(MumblePluginFunctions::getName)>(m_lib.resolve("mumble_getName"));
+ m_pluginFnc.getAPIVersion = reinterpret_cast<decltype(MumblePluginFunctions::getAPIVersion)>(m_lib.resolve("mumble_getAPIVersion"));
+ m_pluginFnc.registerAPIFunctions = reinterpret_cast<decltype(MumblePluginFunctions::registerAPIFunctions)>(m_lib.resolve("mumble_registerAPIFunctions"));
+ m_pluginFnc.releaseResource = reinterpret_cast<decltype(MumblePluginFunctions::releaseResource)>(m_lib.resolve("mumble_releaseResource"));
+
+ // validate that all those functions are available in the loaded lib
+ m_pluginIsValid = m_pluginFnc.init && m_pluginFnc.shutdown && m_pluginFnc.getName && m_pluginFnc.getAPIVersion
+ && m_pluginFnc.registerAPIFunctions && m_pluginFnc.releaseResource;
+
+ if (!m_pluginIsValid) {
+ // Don't bother trying to resolve any other functions
+#ifdef MUMBLE_PLUGIN_DEBUG
+#define CHECK_AND_LOG(name) if (!m_pluginFnc.name) { qDebug("\t\"%s\" is missing the %s() function", qPrintable(m_pluginPath), "mumble_" #name); }
+ CHECK_AND_LOG(init);
+ CHECK_AND_LOG(shutdown);
+ CHECK_AND_LOG(getName);
+ CHECK_AND_LOG(getAPIVersion);
+ CHECK_AND_LOG(registerAPIFunctions);
+ CHECK_AND_LOG(releaseResource);
+#undef CHECK_AND_LOG
+#endif
+
+ return;
+ }
+
+ // The mandatory functions are there, now see if any optional functions are implemented as well
+ m_pluginFnc.setMumbleInfo = reinterpret_cast<decltype(MumblePluginFunctions::setMumbleInfo)>(m_lib.resolve("mumble_setMumbleInfo"));
+ m_pluginFnc.getVersion = reinterpret_cast<decltype(MumblePluginFunctions::getVersion)>(m_lib.resolve("mumble_getVersion"));
+ m_pluginFnc.getAuthor = reinterpret_cast<decltype(MumblePluginFunctions::getAuthor)>(m_lib.resolve("mumble_getAuthor"));
+ m_pluginFnc.getDescription = reinterpret_cast<decltype(MumblePluginFunctions::getDescription)>(m_lib.resolve("mumble_getDescription"));
+ m_pluginFnc.getFeatures = reinterpret_cast<decltype(MumblePluginFunctions::getFeatures)>(m_lib.resolve("mumble_getFeatures"));
+ m_pluginFnc.deactivateFeatures = reinterpret_cast<decltype(MumblePluginFunctions::deactivateFeatures)>(m_lib.resolve("mumble_deactivateFeatures"));
+ m_pluginFnc.initPositionalData = reinterpret_cast<decltype(MumblePluginFunctions::initPositionalData)>(m_lib.resolve("mumble_initPositionalData"));
+ m_pluginFnc.fetchPositionalData = reinterpret_cast<decltype(MumblePluginFunctions::fetchPositionalData)>(m_lib.resolve("mumble_fetchPositionalData"));
+ m_pluginFnc.shutdownPositionalData = reinterpret_cast<decltype(MumblePluginFunctions::shutdownPositionalData)>(m_lib.resolve("mumble_shutdownPositionalData"));
+ m_pluginFnc.onServerConnected = reinterpret_cast<decltype(MumblePluginFunctions::onServerConnected)>(m_lib.resolve("mumble_onServerConnected"));
+ m_pluginFnc.onServerDisconnected = reinterpret_cast<decltype(MumblePluginFunctions::onServerDisconnected)>(m_lib.resolve("mumble_onServerDisconnected"));
+ m_pluginFnc.onChannelEntered = reinterpret_cast<decltype(MumblePluginFunctions::onChannelEntered)>(m_lib.resolve("mumble_onChannelEntered"));
+ m_pluginFnc.onChannelExited = reinterpret_cast<decltype(MumblePluginFunctions::onChannelExited)>(m_lib.resolve("mumble_onChannelExited"));
+ m_pluginFnc.onUserTalkingStateChanged = reinterpret_cast<decltype(MumblePluginFunctions::onUserTalkingStateChanged)>(m_lib.resolve("mumble_onUserTalkingStateChanged"));
+ m_pluginFnc.onReceiveData = reinterpret_cast<decltype(MumblePluginFunctions::onReceiveData)>(m_lib.resolve("mumble_onReceiveData"));
+ m_pluginFnc.onAudioInput = reinterpret_cast<decltype(MumblePluginFunctions::onAudioInput)>(m_lib.resolve("mumble_onAudioInput"));
+ m_pluginFnc.onAudioSourceFetched = reinterpret_cast<decltype(MumblePluginFunctions::onAudioSourceFetched)>(m_lib.resolve("mumble_onAudioSourceFetched"));
+ m_pluginFnc.onAudioOutputAboutToPlay = reinterpret_cast<decltype(MumblePluginFunctions::onAudioOutputAboutToPlay)>(m_lib.resolve("mumble_onAudioOutputAboutToPlay"));
+ m_pluginFnc.onServerSynchronized = reinterpret_cast<decltype(MumblePluginFunctions::onServerSynchronized)>(m_lib.resolve("mumble_onServerSynchronized"));
+ m_pluginFnc.onUserAdded = reinterpret_cast<decltype(MumblePluginFunctions::onUserAdded)>(m_lib.resolve("mumble_onUserAdded"));
+ m_pluginFnc.onUserRemoved = reinterpret_cast<decltype(MumblePluginFunctions::onUserRemoved)>(m_lib.resolve("mumble_onUserRemoved"));
+ m_pluginFnc.onChannelAdded = reinterpret_cast<decltype(MumblePluginFunctions::onChannelAdded)>(m_lib.resolve("mumble_onChannelAdded"));
+ m_pluginFnc.onChannelRemoved = reinterpret_cast<decltype(MumblePluginFunctions::onChannelRemoved)>(m_lib.resolve("mumble_onChannelRemoved"));
+ m_pluginFnc.onChannelRenamed = reinterpret_cast<decltype(MumblePluginFunctions::onChannelRenamed)>(m_lib.resolve("mumble_onChannelRenamed"));
+ m_pluginFnc.onKeyEvent = reinterpret_cast<decltype(MumblePluginFunctions::onKeyEvent)>(m_lib.resolve("mumble_onKeyEvent"));
+ m_pluginFnc.hasUpdate = reinterpret_cast<decltype(MumblePluginFunctions::hasUpdate)>(m_lib.resolve("mumble_hasUpdate"));
+ m_pluginFnc.getUpdateDownloadURL = reinterpret_cast<decltype(MumblePluginFunctions::getUpdateDownloadURL)>(m_lib.resolve("mumble_getUpdateDownloadURL"));
+
+#ifdef MUMBLE_PLUGIN_DEBUG
+#define CHECK_AND_LOG(name) qDebug("\t" "mumble_" #name ": %s", (m_pluginFnc.name == nullptr ? "no" : "yes"))
+ qDebug(">>>> Found optional functions for plugin \"%s\"", qUtf8Printable(m_pluginPath));
+ CHECK_AND_LOG(setMumbleInfo);
+ CHECK_AND_LOG(getVersion);
+ CHECK_AND_LOG(getAuthor);
+ CHECK_AND_LOG(getDescription);
+ CHECK_AND_LOG(getFeatures);
+ CHECK_AND_LOG(deactivateFeatures);
+ CHECK_AND_LOG(initPositionalData);
+ CHECK_AND_LOG(fetchPositionalData);
+ CHECK_AND_LOG(shutdownPositionalData);
+ CHECK_AND_LOG(onServerConnected);
+ CHECK_AND_LOG(onServerDisconnected);
+ CHECK_AND_LOG(onChannelEntered);
+ CHECK_AND_LOG(onChannelExited);
+ CHECK_AND_LOG(onUserTalkingStateChanged);
+ CHECK_AND_LOG(onReceiveData);
+ CHECK_AND_LOG(onAudioInput);
+ CHECK_AND_LOG(onAudioSourceFetched);
+ CHECK_AND_LOG(onAudioOutputAboutToPlay);
+ CHECK_AND_LOG(onServerSynchronized);
+ CHECK_AND_LOG(onUserAdded);
+ CHECK_AND_LOG(onUserRemoved);
+ CHECK_AND_LOG(onChannelAdded);
+ CHECK_AND_LOG(onChannelRemoved);
+ CHECK_AND_LOG(onChannelRenamed);
+ CHECK_AND_LOG(onKeyEvent);
+ CHECK_AND_LOG(hasUpdate);
+ CHECK_AND_LOG(getUpdateDownloadURL);
+ qDebug("<<<<");
+#endif
+
+ // If positional audio is to be supported, all three corresponding functions have to be implemented
+ // For PA it is all or nothing
+ if (!(m_pluginFnc.initPositionalData && m_pluginFnc.fetchPositionalData && m_pluginFnc.shutdownPositionalData)
+ && (m_pluginFnc.initPositionalData || m_pluginFnc.fetchPositionalData || m_pluginFnc.shutdownPositionalData)) {
+ m_pluginFnc.initPositionalData = nullptr;
+ m_pluginFnc.fetchPositionalData = nullptr;
+ m_pluginFnc.shutdownPositionalData = nullptr;
+#ifdef MUMBLE_PLUGIN_DEBUG
+ qDebug("\t\"%s\" has only partially implemented positional data functions -> deactivating all of them", qPrintable(m_pluginPath));
+#endif
+ }
+ }
+}
+
+bool Plugin::isValid() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_pluginIsValid;
+}
+
+bool Plugin::isLoaded() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_pluginIsLoaded;
+}
+
+plugin_id_t Plugin::getID() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_pluginID;
+}
+
+bool Plugin::isBuiltInPlugin() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_isBuiltIn;
+}
+
+QString Plugin::getFilePath() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_pluginPath;
+}
+
+bool Plugin::isPositionalDataEnabled() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_positionalDataIsEnabled;
+}
+
+void Plugin::enablePositionalData(bool enable) {
+ QWriteLocker lock(&m_pluginLock);
+
+ m_positionalDataIsEnabled = enable;
+}
+
+bool Plugin::isPositionalDataActive() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_positionalDataIsActive;
+}
+
+void Plugin::allowKeyboardMonitoring(bool allow) {
+ QWriteLocker lock(&m_pluginLock);
+
+ m_mayMonitorKeyboard = allow;
+}
+
+bool Plugin::isKeyboardMonitoringAllowed() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ return m_mayMonitorKeyboard;
+}
+
+mumble_error_t Plugin::init() {
+ {
+ QReadLocker lock(&m_pluginLock);
+
+ if (m_pluginIsLoaded) {
+ return STATUS_OK;
+ }
+ }
+
+ //////////////////////////////
+ // Step 1: Introduce ourselves (inform the plugin about Mumble's (API) version
+
+ // Get Mumble version
+ int mumbleMajor, mumbleMinor, mumblePatch;
+ MumbleVersion::get(&mumbleMajor, &mumbleMinor, &mumblePatch);
+
+ // Require API version 1.0.0 as the minimal supported one
+ setMumbleInfo({ mumbleMajor, mumbleMinor, mumblePatch }, MUMBLE_PLUGIN_API_VERSION, { 1, 0, 0 });
+
+
+ //////////////////////////////
+ // Step 2: Provide the API functions to the plugin
+ const mumble_version_t apiVersion = getAPIVersion();
+ if (apiVersion >= mumble_version_t({1, 0, 0}) && apiVersion < mumble_version_t({1, 2, 0})) {
+ MumbleAPI_v_1_0_x api = API::getMumbleAPI_v_1_0_x();
+ registerAPIFunctions(&api);
+ } else {
+ // The API version could not be obtained -> this is an invalid plugin that shouldn't have been loaded in the first place
+ qWarning("Unable to obtain requested MumbleAPI version");
+ return EC_INVALID_API_VERSION;
+ }
+
+
+ //////////////////////////////
+ // Step 3: Actually try to load the plugin
+
+ mumble_error_t retStatus;
+ if (m_pluginFnc.init) {
+ retStatus = m_pluginFnc.init(m_pluginID);
+ } else {
+ retStatus = EC_GENERIC_ERROR;
+ }
+
+ {
+ QWriteLocker lock(&m_pluginLock);
+ m_pluginIsLoaded = retStatus == STATUS_OK;
+ }
+
+ return retStatus;
+}
+
+void Plugin::shutdown() {
+ bool posDataActive;
+ {
+ QReadLocker rLock(&m_pluginLock);
+ if (!m_pluginIsLoaded) {
+ return;
+ }
+
+ posDataActive = m_positionalDataIsActive;
+ }
+
+ if (posDataActive) {
+ shutdownPositionalData();
+ }
+
+ if (m_pluginFnc.shutdown) {
+ m_pluginFnc.shutdown();
+ }
+
+ {
+ QWriteLocker lock(&m_pluginLock);
+
+ m_pluginIsLoaded = false;
+ }
+}
+
+QString Plugin::getName() const {
+ if (m_pluginFnc.getName) {
+ return extractWrappedString(m_pluginFnc.getName());
+ } else {
+ return QString::fromLatin1("Unknown plugin");
+ }
+}
+
+mumble_version_t Plugin::getAPIVersion() const {
+ if (m_pluginFnc.getAPIVersion) {
+ return m_pluginFnc.getAPIVersion();
+ } else {
+ return VERSION_UNKNOWN;
+ }
+}
+
+void Plugin::registerAPIFunctions(void *api) const {
+ if (m_pluginFnc.registerAPIFunctions) {
+ m_pluginFnc.registerAPIFunctions(api);
+ }
+}
+
+void Plugin::releaseResource(const void *pointer) const {
+ if (m_pluginFnc.releaseResource) {
+ m_pluginFnc.releaseResource(pointer);
+ }
+}
+
+void Plugin::setMumbleInfo(mumble_version_t mumbleVersion, mumble_version_t mumbleAPIVersion, mumble_version_t minimalExpectedAPIVersion) const {
+ if (m_pluginFnc.setMumbleInfo) {
+ m_pluginFnc.setMumbleInfo(mumbleVersion, mumbleAPIVersion, minimalExpectedAPIVersion);
+ }
+}
+
+mumble_version_t Plugin::getVersion() const {
+ if (m_pluginFnc.getVersion) {
+ return m_pluginFnc.getVersion();
+ } else {
+ return VERSION_UNKNOWN;
+ }
+}
+
+QString Plugin::getAuthor() const {
+ if (m_pluginFnc.getAuthor) {
+ return extractWrappedString(m_pluginFnc.getAuthor());
+ } else {
+ return QString::fromLatin1("Unknown");
+ }
+}
+
+QString Plugin::getDescription() const {
+ if (m_pluginFnc.getDescription) {
+ return extractWrappedString(m_pluginFnc.getDescription());
+ } else {
+ return QString::fromLatin1("No description provided");
+ }
+}
+
+uint32_t Plugin::getFeatures() const {
+ if (m_pluginFnc.getFeatures) {
+ return m_pluginFnc.getFeatures();
+ } else {
+ return FEATURE_NONE;
+ }
+}
+
+uint32_t Plugin::deactivateFeatures(uint32_t features) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.deactivateFeatures) {
+ return m_pluginFnc.deactivateFeatures(features);
+ } else {
+ return features;
+ }
+}
+
+bool Plugin::showAboutDialog(QWidget *parent) const {
+ assertPluginLoaded(this);
+
+ Q_UNUSED(parent);
+ return false;
+}
+
+bool Plugin::showConfigDialog(QWidget *parent) const {
+ assertPluginLoaded(this);
+
+ Q_UNUSED(parent);
+ return false;
+}
+
+uint8_t Plugin::initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.initPositionalData) {
+ uint8_t returnCode = m_pluginFnc.initPositionalData(programNames, programPIDs, programCount);
+
+ {
+ QWriteLocker lock(&m_pluginLock);
+ m_positionalDataIsActive = returnCode == PDEC_OK;
+ }
+
+ return returnCode;
+ } else {
+ return PDEC_ERROR_PERM;
+ }
+}
+
+bool Plugin::fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir,
+ Vector3D& cameraAxis, QString& context, QString& identity) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.fetchPositionalData) {
+ const char *contextPtr = "";
+ const char *identityPtr = "";
+
+ bool retStatus = m_pluginFnc.fetchPositionalData(static_cast<float*>(avatarPos), static_cast<float*>(avatarDir),
+ static_cast<float*>(avatarAxis), static_cast<float*>(cameraPos), static_cast<float*>(cameraDir), static_cast<float*>(cameraAxis),
+ &contextPtr, &identityPtr);
+
+ context = QString::fromUtf8(contextPtr);
+ identity = QString::fromUtf8(identityPtr);
+
+ return retStatus;
+ } else {
+ avatarPos.toZero();
+ avatarDir.toZero();
+ avatarAxis.toZero();
+ cameraPos.toZero();
+ cameraDir.toZero();
+ cameraAxis.toZero();
+ context = QString();
+ identity = QString();
+
+ return false;
+ }
+}
+
+void Plugin::shutdownPositionalData() {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.shutdownPositionalData) {
+ m_positionalDataIsActive = false;
+
+ m_pluginFnc.shutdownPositionalData();
+ }
+}
+
+void Plugin::onServerConnected(mumble_connection_t connection) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onServerConnected) {
+ m_pluginFnc.onServerConnected(connection);
+ }
+}
+
+void Plugin::onServerDisconnected(mumble_connection_t connection) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onServerDisconnected) {
+ m_pluginFnc.onServerDisconnected(connection);
+ }
+}
+
+void Plugin::onChannelEntered(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t previousChannelID,
+ mumble_channelid_t newChannelID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onChannelEntered) {
+ m_pluginFnc.onChannelEntered(connection, userID, previousChannelID, newChannelID);
+ }
+}
+
+void Plugin::onChannelExited(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t channelID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onChannelExited) {
+ m_pluginFnc.onChannelExited(connection, userID, channelID);
+ }
+}
+
+void Plugin::onUserTalkingStateChanged(mumble_connection_t connection, mumble_userid_t userID, mumble_talking_state_t talkingState) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onUserTalkingStateChanged) {
+ m_pluginFnc.onUserTalkingStateChanged(connection, userID, talkingState);
+ }
+}
+
+bool Plugin::onReceiveData(mumble_connection_t connection, mumble_userid_t sender, const uint8_t *data, size_t dataLength, const char *dataID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onReceiveData) {
+ return m_pluginFnc.onReceiveData(connection, sender, data, dataLength, dataID);
+ } else {
+ return false;
+ }
+}
+
+bool Plugin::onAudioInput(short *inputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onAudioInput) {
+ return m_pluginFnc.onAudioInput(inputPCM, sampleCount, channelCount, sampleRate, isSpeech);
+ } else {
+ return false;
+ }
+}
+
+bool Plugin::onAudioSourceFetched(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech, mumble_userid_t userID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onAudioSourceFetched) {
+ return m_pluginFnc.onAudioSourceFetched(outputPCM, sampleCount, channelCount, sampleRate, isSpeech, userID);
+ } else {
+ return false;
+ }
+}
+
+bool Plugin::onAudioOutputAboutToPlay(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onAudioOutputAboutToPlay) {
+ return m_pluginFnc.onAudioOutputAboutToPlay(outputPCM, sampleCount, channelCount, sampleRate);
+ } else {
+ return false;
+ }
+}
+
+void Plugin::onServerSynchronized(mumble_connection_t connection) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onServerSynchronized) {
+ m_pluginFnc.onServerSynchronized(connection);
+ }
+}
+
+void Plugin::onUserAdded(mumble_connection_t connection, mumble_userid_t userID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onUserAdded) {
+ m_pluginFnc.onUserAdded(connection, userID);
+ }
+}
+
+void Plugin::onUserRemoved(mumble_connection_t connection, mumble_userid_t userID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onUserRemoved) {
+ m_pluginFnc.onUserRemoved(connection, userID);
+ }
+}
+
+void Plugin::onChannelAdded(mumble_connection_t connection, mumble_channelid_t channelID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onChannelAdded) {
+ m_pluginFnc.onChannelAdded(connection, channelID);
+ }
+}
+
+void Plugin::onChannelRemoved(mumble_connection_t connection, mumble_channelid_t channelID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onChannelRemoved) {
+ m_pluginFnc.onChannelRemoved(connection, channelID);
+ }
+}
+
+void Plugin::onChannelRenamed(mumble_connection_t connection, mumble_channelid_t channelID) const {
+ assertPluginLoaded(this);
+
+ if (m_pluginFnc.onChannelRenamed) {
+ m_pluginFnc.onChannelRenamed(connection, channelID);
+ }
+}
+
+void Plugin::onKeyEvent(mumble_keycode_t keyCode, bool wasPress) const {
+ assertPluginLoaded(this);
+
+ if (!m_mayMonitorKeyboard) {
+ // Keyboard monitoring is forbidden for this plugin
+ return;
+ }
+
+ if (m_pluginFnc.onKeyEvent) {
+ m_pluginFnc.onKeyEvent(keyCode, wasPress);
+ }
+}
+
+bool Plugin::hasUpdate() const {
+ if (m_pluginFnc.hasUpdate) {
+ return m_pluginFnc.hasUpdate();
+ } else {
+ // A plugin that doesn't implement this function is assumed to never know about
+ // any potential updates
+ return false;
+ }
+}
+
+QUrl Plugin::getUpdateDownloadURL() const {
+ if (m_pluginFnc.getUpdateDownloadURL) {
+ return QUrl(extractWrappedString(m_pluginFnc.getUpdateDownloadURL()));
+ } else {
+ // Return an empty URL as a fallback
+ return QUrl();
+ }
+}
+
+bool Plugin::providesAboutDialog() const {
+ return false;
+}
+
+bool Plugin::providesConfigDialog() const {
+ return false;
+}
+
+
+
+/////////////////// Implementation of the PluginReadLocker /////////////////////////
+PluginReadLocker::PluginReadLocker(QReadWriteLock *lock)
+ : m_lock(lock),
+ m_unlocked(false) {
+ relock();
+}
+
+void PluginReadLocker::unlock() {
+ if (!m_lock) {
+ // do nothgin for nullptr
+ return;
+ }
+
+ m_unlocked = true;
+
+ m_lock->unlock();
+}
+
+void PluginReadLocker::relock() {
+ if (!m_lock) {
+ // do nothing for a nullptr
+ return;
+ }
+
+ // First try to lock for read-access
+ if (!m_lock->tryLockForRead()) {
+ // if that fails, we'll try to lock for write-access
+ // That will only succeed in the case that the current thread holds the write-access to this lock already which caused
+ // the previous attempt to lock for reading to fail (by design of the QtReadWriteLock).
+ // As we are in the thread with the write-access, it means that this threads has asked for read-access on top of it which we will
+ // grant (in contrast of QtReadLocker) because if you have the permission to change something you surely should have permission
+ // to read it. This assumes that the thread won't try to read data it temporarily has corrupted.
+ if (!m_lock->tryLockForWrite()) {
+ // If we couldn't lock for write at this point, it means another thread has write-access granted by the lock so we'll have to wait
+ // in order to gain regular read-access as would be with QtReadLocker
+ m_lock->lockForRead();
+ }
+ }
+
+ m_unlocked = false;
+}
+
+PluginReadLocker::~PluginReadLocker() {
+ if (m_lock && !m_unlocked) {
+ // unlock the lock if it isn't nullptr
+ m_lock->unlock();
+ }
+}
diff --git a/src/mumble/Plugin.h b/src/mumble/Plugin.h
new file mode 100644
index 000000000..049e622f2
--- /dev/null
+++ b/src/mumble/Plugin.h
@@ -0,0 +1,417 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGIN_H_
+#define MUMBLE_MUMBLE_PLUGIN_H_
+
+#include "MumbleAPI_v_1_0_x.h"
+#include "PluginComponents_v_1_0_x.h"
+#include "PositionalData.h"
+#include "MumblePlugin_v_1_0_x.h"
+
+#include <QObject>
+#include <QReadWriteLock>
+#include <QString>
+#include <QLibrary>
+#include <QMutex>
+#include <QUrl>
+
+#include <stdexcept>
+#include <memory>
+
+/// A struct for holding the function pointers to the functions inside the plugin's library
+/// For the documentation of those functions, see the plugin's header file (the one used when developing a plugin)
+struct MumblePluginFunctions {
+ decltype(&mumble_init) init;
+ decltype(&mumble_shutdown) shutdown;
+ decltype(&mumble_getName) getName;
+ decltype(&mumble_getAPIVersion) getAPIVersion;
+ decltype(&mumble_registerAPIFunctions) registerAPIFunctions;
+ decltype(&mumble_releaseResource) releaseResource;
+
+ // Further utility functions the plugin may implement
+ decltype(&mumble_setMumbleInfo) setMumbleInfo;
+ decltype(&mumble_getVersion) getVersion;
+ decltype(&mumble_getAuthor) getAuthor;
+ decltype(&mumble_getDescription) getDescription;
+ decltype(&mumble_getFeatures) getFeatures;
+ decltype(&mumble_deactivateFeatures) deactivateFeatures;
+
+ // Functions for dealing with positional audio (or rather the fetching of the needed data)
+ decltype(&mumble_initPositionalData) initPositionalData;
+ decltype(&mumble_fetchPositionalData) fetchPositionalData;
+ decltype(&mumble_shutdownPositionalData) shutdownPositionalData;
+
+ // Callback functions and EventHandlers
+ decltype(&mumble_onServerConnected) onServerConnected;
+ decltype(&mumble_onServerDisconnected) onServerDisconnected;
+ decltype(&mumble_onChannelEntered) onChannelEntered;
+ decltype(&mumble_onChannelExited) onChannelExited;
+ decltype(&mumble_onUserTalkingStateChanged) onUserTalkingStateChanged;
+ decltype(&mumble_onReceiveData) onReceiveData;
+ decltype(&mumble_onAudioInput) onAudioInput;
+ decltype(&mumble_onAudioSourceFetched) onAudioSourceFetched;
+ decltype(&mumble_onAudioOutputAboutToPlay) onAudioOutputAboutToPlay;
+ decltype(&mumble_onServerSynchronized) onServerSynchronized;
+ decltype(&mumble_onUserAdded) onUserAdded;
+ decltype(&mumble_onUserRemoved) onUserRemoved;
+ decltype(&mumble_onChannelAdded) onChannelAdded;
+ decltype(&mumble_onChannelRemoved) onChannelRemoved;
+ decltype(&mumble_onChannelRenamed) onChannelRenamed;
+ decltype(&mumble_onKeyEvent) onKeyEvent;
+
+ // Plugin updates
+ decltype(&mumble_hasUpdate) hasUpdate;
+ decltype(&mumble_getUpdateDownloadURL) getUpdateDownloadURL;
+};
+
+
+/// An exception that is being thrown by a plugin whenever it encounters an error
+class PluginError : public std::runtime_error {
+ public:
+ // inherit constructors of runtime_error
+ using std::runtime_error::runtime_error;
+};
+
+
+/// An implementation similar to QReadLocker except that this one allows to lock on a lock the same thread is already
+/// holding a write-lock on. This could also result in obtaining a write-lock though so it shouldn't be used for code regions
+/// that take quite some time and rely on other readers still having access to the locked object.
+class PluginReadLocker {
+ protected:
+ /// The lock this lock-guard is acting upon
+ QReadWriteLock *m_lock;
+ /// A flag indicating whether the lock has been unlocked (manually) and thus doesn't have to be unlocked
+ /// in the destructor.
+ bool m_unlocked;
+ public:
+ /// Constructor of the PluginReadLocker. If the passed lock-pointer is not nullptr, the constructor will
+ /// already lock the provided lock.
+ ///
+ /// @param lock A pointer to the QReadWriteLock that shall be managed by this object. May be nullptr
+ PluginReadLocker(QReadWriteLock *lock);
+ /// Locks this lock again after it has been unlocked before (Locking a locked lock results in a runtime error)
+ void relock();
+ /// Unlocks this lock
+ void unlock();
+ ~PluginReadLocker();
+};
+
+class Plugin;
+
+/// Typedef for the plugin ID
+typedef uint32_t plugin_id_t;
+/// Typedef for a plugin pointer
+typedef std::shared_ptr<Plugin> plugin_ptr_t;
+/// Typedef for a const plugin pointer
+typedef std::shared_ptr<const Plugin> const_plugin_ptr_t;
+
+/// A class representing a plugin library attached to Mumble. It can be used to manage (load/unload) and access plugin libraries.
+class Plugin : public QObject {
+ friend class PluginManager;
+ friend class PluginConfig;
+
+ private:
+ Q_OBJECT
+ Q_DISABLE_COPY(Plugin)
+ protected:
+ /// A mutex guarding Plugin::nextID
+ static QMutex s_idLock;
+ /// The ID of the plugin that will be loaded next. Whenever accessing this field, Plugin::idLock should be locked.
+ static plugin_id_t s_nextID;
+
+ /// Constructor of the Plugin.
+ ///
+ /// @param path The path to the plugin's shared library file. This path has to exist unless isBuiltIn is true
+ /// @param isBuiltIn A flag indicating that this is a plugin built into Mumble itself and is does not backed by a shared library
+ /// @param p A pointer to a QObject representing the parent of this object or nullptr if there is no parent
+ Plugin(QString path, bool isBuiltIn = false, QObject *p = nullptr);
+
+ /// A flag indicating whether this plugin is valid. It is mainly used throughout the plugin's initialization.
+ bool m_pluginIsValid;
+ /// The QLibrary representing the shared library of this plugin
+ QLibrary m_lib;
+ /// The path to the shared library file in the host's filesystem
+ QString m_pluginPath;
+ /// The unique ID of this plugin. Note though that this ID is not suitable for uniquely identifying this plugin between restarts of Mumble
+ /// (not even between rescans of the plugins) let alone across clients.
+ plugin_id_t m_pluginID;
+ // a flag indicating whether this plugin has been loaded by calling its init function.
+ bool m_pluginIsLoaded;
+ /// The lock guarding this plugin object. Every time a member is accessed this lock should be locked accordingly.
+ /// After successful construction and initialization (doInitilize()), this member variable is effectively const
+ /// and therefore no locking is required in order to read from it!
+ /// In fact protecting read-accesses by a non-recursive lock can introduce deadlocks by plugins using certain
+ /// API functions.
+ mutable QReadWriteLock m_pluginLock;
+ /// The struct holding the function pointers to the functions in the shared library.
+ MumblePluginFunctions m_pluginFnc;
+ /// A flag indicating whether this plugin is built into Mumble and is thus not represented by a shared library.
+ bool m_isBuiltIn;
+ /// A flag indicating whether positional data gathering is enabled for this plugin (Enabled as in allowed via preferences).
+ bool m_positionalDataIsEnabled;
+ /// A flag indicating whether positional data gathering is currently active (Active as in running)
+ bool m_positionalDataIsActive;
+ /// A flag indicating whether this plugin has permission to monitor keyboard events that occur while
+ /// Mumble has the keyboard focus.
+ bool m_mayMonitorKeyboard;
+
+
+ QString extractWrappedString(MumbleStringWrapper wrapper) const;
+
+
+ // Most of this class's functions are protected in order to only allow access to them via the PluginManager
+ // as some require additional handling before/after calling them.
+
+ /// Initializes this plugin. This function must be called directly after construction. This is guaranteed when the
+ /// plugin is created via Plugin::createNew
+ virtual bool doInitialize();
+ /// Resolves the function pointers in the shared library and sets the respective fields in Plugin::apiFnc
+ virtual void resolveFunctionPointers();
+ /// Enables positional data gathering for this plugin (as in allowing)
+ ///
+ /// @param enable Whether to enable the data gathering
+ virtual void enablePositionalData(bool enable = true);
+ /// Allows or forbids the monitoring of keyboard events for this plugin.
+ ///
+ /// @param allow Whether to allow or forbid it
+ virtual void allowKeyboardMonitoring(bool allow);
+
+
+ /// Initializes this plugin
+ virtual mumble_error_t init();
+ /// Shuts this plugin down
+ virtual void shutdown();
+ /// Delegates the struct of API function pointers to the plugin backend
+ ///
+ /// @param api The pointer to the API struct
+ virtual void registerAPIFunctions(void *api) const;
+ /// Asks the plugin to release (free/delete) the resource pointed to by the given pointer
+ ///
+ /// @param pointer Pointer to the resource
+ virtual void releaseResource(const void *pointer) const;
+ /// Provides the plugin backend with some version information about Mumble
+ ///
+ /// @param mumbleVersion The version of the Mumble client
+ /// @param mumbleAPIVersion The API version used by the Mumble client
+ /// @param minimalExpectedAPIVersion The minimal API version expected to be used by the plugin backend
+ virtual void setMumbleInfo(mumble_version_t mumbleVersion, mumble_version_t mumbleAPIVersion, mumble_version_t minimalExpectedAPIVersion) const;
+ /// Asks the plugin to deactivate certain features
+ ///
+ /// @param features The feature list or'ed together
+ /// @returns The list of features that couldn't be deactivated or'ed together
+ virtual uint32_t deactivateFeatures(uint32_t features) const;
+ /// Shows an about-dialog
+ ///
+ /// @parent A pointer to the QWidget that should be used as a parent
+ /// @returns Whether the dialog could be shown successfully
+ virtual bool showAboutDialog(QWidget *parent) const;
+ /// Shows a config-dialog
+ ///
+ /// @parent A pointer to the QWidget that should be used as a parent
+ /// @returns Whether the dialog could be shown successfully
+ virtual bool showConfigDialog(QWidget *parent) const;
+ /// Initializes the positional data gathering
+ ///
+ /// @params programNames A pointer to an array of const char* representing the program names
+ /// @params programCount A pointer to an array of PIDs corresponding to the program names
+ /// @params programCount The length of the two previous arrays
+ virtual uint8_t initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount);
+ /// Fetches the positional data
+ ///
+ /// @param[out] avatarPos The position of the ingame avatar (player)
+ /// @param[out] avatarDir The directiion in which the avatar (player) is looking/facing
+ /// @param[out] avatarAxis The vector from the avatar's toes to its head
+ /// @param[out] cameraPos The position of the ingame camera
+ /// @param[out] cameraDir The direction in which the camera is looking/facing
+ /// @param[out] cameraAxis The vector from the camera's bottom to its top
+ /// @param[out] context The context of the current game-session (includes server/squad info)
+ /// @param[out] identity The ingame identity of the player (name)
+ virtual bool fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir,
+ Vector3D& cameraAxis, QString& context, QString& identity) const;
+ /// Shuts down positional data gathering
+ virtual void shutdownPositionalData();
+ /// Called to indicate that the client has connected to a server
+ ///
+ /// @param connection An object used to identify the current connection
+ virtual void onServerConnected(mumble_connection_t connection) const;
+ /// Called to indicate that the client disconnected from a server
+ ///
+ /// @param connection An object used to identify the connection that has been disconnected
+ virtual void onServerDisconnected(mumble_connection_t connection) const;
+ /// Called to indicate that a user has switched its channel
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that switched channel
+ /// @param previousChannelID The ID of the channel the user came from (-1 if there is no previous channel)
+ /// æparam newChannelID The ID of the channel the user has switched to
+ virtual void onChannelEntered(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t previousChannelID,
+ mumble_channelid_t newChannelID) const;
+ /// Called to indicate that a user exited a channel.
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that switched channel
+ /// @param channelID The ID of the channel the user exited
+ virtual void onChannelExited(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t channelID) const;
+ /// Called to indicate that a user has changed its talking state
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that switched channel
+ /// @param talkingState The new talking state of the user
+ virtual void onUserTalkingStateChanged(mumble_connection_t connection, mumble_userid_t userID, mumble_talking_state_t talkingState) const;
+ /// Called to indicate that a data packet has been received
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param sender The ID of the user whose client sent the data
+ /// @param data The actual data
+ /// @param dataLength The length of the data array
+ /// @param datID The ID of the data used to determine whether this plugin handles this data or not
+ /// @returns Whether this plugin handled the data
+ virtual bool onReceiveData(mumble_connection_t connection, mumble_userid_t sender, const uint8_t *data, size_t dataLength, const char *dataID) const;
+ /// Called to indicate that there is audio input
+ ///
+ /// @param inputPCM A pointer to a short array representing the input PCM
+ /// @param sampleCount The amount of samples per channel
+ /// @param channelCount The amount of channels in the PCM
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers the input as speech
+ /// @returns Whether this pluign has modified the audio
+ virtual bool onAudioInput(short *inputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech) const;
+ /// Called to indicate that an audio source has been fetched
+ ///
+ /// @param outputPCM A pointer to a short array representing the output PCM
+ /// @param sampleCount The amount of samples per channel
+ /// @param channelCount The amount of channels in the PCM
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers the output as speech
+ /// @param userID The ID of the user responsible for the output (only relevant if isSpeech == true)
+ /// @returns Whether this pluign has modified the audio
+ virtual bool onAudioSourceFetched(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech, mumble_userid_t userID) const;
+ /// Called to indicate that audio is about to be played
+ ///
+ /// @param outputPCM A pointer to a short array representing the output PCM
+ /// @param sampleCount The amount of samples per channel
+ /// @param channelCount The amount of channels in the PCM
+ /// @param sampleRate The used sample rate in Hz
+ /// @returns Whether this pluign has modified the audio
+ virtual bool onAudioOutputAboutToPlay(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate) const;
+ /// Called when the server has synchronized with the client
+ ///
+ /// @param connection An object used to identify the current connection
+ virtual void onServerSynchronized(mumble_connection_t connection) const;
+ /// Called when a new user gets added to the user model. This is the case when that new user freshly connects to the server the
+ /// local user is on but also when the local user connects to a server other clients are already connected to (in this case this
+ /// method will be called for every client already on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that has been added
+ virtual void onUserAdded(mumble_connection_t connection, mumble_userid_t userID) const;
+ /// Called when a user gets removed from the user model. This is the case when that user disconnects from the server the
+ /// local user is on but also when the local user disconnects from a server other clients are connected to (in this case this
+ /// method will be called for every client on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param userID The ID of the user that has been removed
+ virtual void onUserRemoved(mumble_connection_t connection, mumble_userid_t userID) const;
+ /// Called when a new channel gets added to the user model. This is the case when a new channel is created on the server the local
+ /// user is on but also when the local user connects to a server that contains channels other than the root-channel (in this case
+ /// this method will be called for ever non-root channel on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param channelID The ID of the channel that has been added
+ virtual void onChannelAdded(mumble_connection_t connection, mumble_channelid_t channelID) const;
+ /// Called when a channel gets removed from the user model. This is the case when a channel is removed on the server the local
+ /// user is on but also when the local user disconnects from a server that contains channels other than the root-channel (in this case
+ /// this method will be called for ever non-root channel on that server).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param channelID The ID of the channel that has been removed
+ virtual void onChannelRemoved(mumble_connection_t connection, mumble_channelid_t channelID) const;
+ /// Called when a channel gets renamed. This also applies when a new channel is created (thus assigning it an initial name is
+ /// also considered renaming).
+ ///
+ /// @param connection An object used to identify the current connection
+ /// @param channelID The ID of the channel that has been renamed
+ virtual void onChannelRenamed(mumble_connection_t connection, mumble_channelid_t channelID) const;
+ /// Called when a key has been pressed or released while Mumble has keyboard focus.
+ ///
+ /// @param keyCode The key code of the respective key. The character codes are defined
+ /// via the KeyCode enum. For printable 7-bit ASCII characters these codes conform
+ /// to the ASCII code-page with the only difference that case is not distinguished. Therefore
+ /// always the upper-case letter code will be used for letters.
+ /// @param wasPress Whether the key has been pressed (instead of released)
+ virtual void onKeyEvent(mumble_keycode_t keyCode, bool wasPress) const;
+
+
+ public:
+ /// A template function for instantiating new plugin objects and initializing them. The plugin will be allocated on the heap and has
+ /// thus to be deleted via the delete instruction.
+ ///
+ /// @tparam T The type of the plugin to be instantiated
+ /// @tparam Ts The types of the contructor arguments
+ /// @param args A list of args passed to the contructor of the plugin object
+ /// @returns A pointer to the allocated plugin
+ ///
+ /// @throws PluginError if the plugin could not be loaded
+ template<typename T, typename ... Ts>
+ static T* createNew(Ts&&...args) {
+ static_assert(std::is_base_of<Plugin, T>::value, "The Plugin::create() can only be used to instantiate objects of base-type Plugin");
+ static_assert(!std::is_pointer<T>::value, "Plugin::create() can't be used to instantiate pointers. It will return a pointer automatically");
+
+ T *instancePtr = new T(std::forward<Ts>(args)...);
+
+ // call the initialize-method and throw an exception of it doesn't succeed
+ if (!instancePtr->doInitialize()) {
+ delete instancePtr;
+ // Delete the constructed object to prevent a memory leak
+ throw PluginError("Failed to initialize plugin");
+ }
+
+ return instancePtr;
+ }
+
+ /// Destructor
+ virtual ~Plugin() Q_DECL_OVERRIDE;
+ /// @returns Whether this plugin is in a valid state
+ virtual bool isValid() const;
+ /// @returns Whether this plugin is loaded (has been initialized via Plugin::init())
+ virtual bool isLoaded() const Q_DECL_FINAL;
+ /// @returns The unique ID of this plugin. This ID holds only as long as this plugin isn't "reconstructed".
+ virtual plugin_id_t getID() const Q_DECL_FINAL;
+ /// @returns Whether this plugin is built into Mumble (thus not backed by a shared library)
+ virtual bool isBuiltInPlugin() const Q_DECL_FINAL;
+ /// @returns The path to the shared library in the host's filesystem
+ virtual QString getFilePath() const;
+ /// @returns Whether positional data gathering is enabled (as in allowed via preferences)
+ virtual bool isPositionalDataEnabled() const Q_DECL_FINAL;
+ /// @returns Whether positional data gathering is currently active (as in running)
+ virtual bool isPositionalDataActive() const Q_DECL_FINAL;
+ /// @returns Whether this plugin is currently allowed to monitor keyboard events
+ virtual bool isKeyboardMonitoringAllowed() const Q_DECL_FINAL;
+
+
+ /// @returns Whether this plugin provides an about-dialog
+ virtual bool providesAboutDialog() const;
+ /// @returns Whether this plugin provides an config-dialog
+ virtual bool providesConfigDialog() const;
+ /// @returns The name of this plugin
+ virtual QString getName() const;
+ /// @returns The API version this plugin intends to use
+ virtual mumble_version_t getAPIVersion() const;
+ /// @returns The version of this plugin
+ virtual mumble_version_t getVersion() const;
+ /// @returns The author of this plugin
+ virtual QString getAuthor() const;
+ /// @returns The plugin's description
+ virtual QString getDescription() const;
+ /// @returns The plugin's features or'ed together (See the PluginFeature enum in MumblePlugin.h for what features are available)
+ virtual uint32_t getFeatures() const;
+ /// @return Whether the plugin has found a new/updated version of itself available for download
+ virtual bool hasUpdate() const;
+ /// @return The URL to download the updated plugin. May be empty
+ virtual QUrl getUpdateDownloadURL() const;
+};
+
+#endif
diff --git a/src/mumble/PluginConfig.cpp b/src/mumble/PluginConfig.cpp
new file mode 100644
index 000000000..e70877067
--- /dev/null
+++ b/src/mumble/PluginConfig.cpp
@@ -0,0 +1,247 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "PluginConfig.h"
+
+#include "Log.h"
+#include "MainWindow.h"
+#include "Message.h"
+#include "ServerHandler.h"
+#include "WebFetch.h"
+#include "MumbleApplication.h"
+#include "ManualPlugin.h"
+#include "Utils.h"
+#include "PluginInstaller.h"
+#include "PluginManager.h"
+
+#include <QtWidgets/QMessageBox>
+#include <QtCore/QUrl>
+#include <QtCore/QDir>
+#include <QtCore/QStandardPaths>
+#include <QtWidgets/QFileDialog>
+#include "Global.h"
+
+const QString PluginConfig::name = QLatin1String("PluginConfig");
+
+static ConfigWidget *PluginConfigDialogNew(Settings &st) {
+ return new PluginConfig(st);
+}
+
+static ConfigRegistrar registrarPluginConfig(5000, PluginConfigDialogNew);
+
+
+PluginConfig::PluginConfig(Settings &st) : ConfigWidget(st) {
+ setupUi(this);
+
+ qtwPlugins->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ qtwPlugins->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
+ qtwPlugins->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
+ qtwPlugins->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
+
+ qpbUnload->setEnabled(false);
+
+ refillPluginList();
+}
+
+QString PluginConfig::title() const {
+ return tr("Plugins");
+}
+
+const QString &PluginConfig::getName() const {
+ return PluginConfig::name;
+}
+
+QIcon PluginConfig::icon() const {
+ return QIcon(QLatin1String("skin:config_plugin.png"));
+}
+
+void PluginConfig::load(const Settings &r) {
+ loadCheckBox(qcbTransmit, r.bTransmitPosition);
+}
+
+void PluginConfig::on_qpbInstallPlugin_clicked() {
+ QString pluginFile = QFileDialog::getOpenFileName(this, tr("Install plugin..."), QDir::homePath());
+
+ if (pluginFile.isEmpty()) {
+ return;
+ }
+
+ try {
+ PluginInstaller installer(pluginFile, this);
+ if (installer.exec() == QDialog::Accepted) {
+ // Reload plugins so the new one actually shows up
+ on_qpbReload_clicked();
+
+ QMessageBox::information(this, "Mumble", tr("The plugin was installed successfully"), QMessageBox::Ok, QMessageBox::NoButton);
+ }
+ } catch (const PluginInstallException &e) {
+ QMessageBox::critical(this, "Mumble", e.getMessage(), QMessageBox::Ok, QMessageBox::NoButton);
+ }
+}
+
+void PluginConfig::save() const {
+ s.bTransmitPosition = qcbTransmit->isChecked();
+ s.qhPluginSettings.clear();
+
+ if (!s.bTransmitPosition) {
+ // Make sure that if posData is currently running, it gets reset
+ // The setting will prevent the system from reactivating
+ Global::get().pluginManager->unlinkPositionalData();
+ }
+
+ constexpr int enableCol = 1;
+ constexpr int positionalDataCol = 2;
+ constexpr int keyboardMonitorCol = 3;
+
+ QList<QTreeWidgetItem *> list = qtwPlugins->findItems(QString(), Qt::MatchContains);
+ for(QTreeWidgetItem *i : list) {
+
+ bool enable = (i->checkState(enableCol) == Qt::Checked);
+ bool positionalDataEnabled = (i->checkState(positionalDataCol) == Qt::Checked);
+ bool keyboardMonitoringEnabled = (i->checkState(keyboardMonitorCol) == Qt::Checked);
+
+ const_plugin_ptr_t plugin = pluginForItem(i);
+ if (plugin) {
+ // insert plugin to settings
+ Global::get().pluginManager->enablePositionalDataFor(plugin->getID(), positionalDataEnabled);
+ Global::get().pluginManager->allowKeyboardMonitoringFor(plugin->getID(), keyboardMonitoringEnabled);
+
+ if (enable) {
+ if (Global::get().pluginManager->loadPlugin(plugin->getID())) {
+ // potentially deactivate plugin features
+ // A plugin's feature is considered to be enabled by default after loading. Thus we only need to
+ // deactivate the ones we don't want
+ uint32_t featuresToDeactivate = FEATURE_NONE;
+ const uint32_t pluginFeatures = plugin->getFeatures();
+
+ if (!positionalDataEnabled && (pluginFeatures & FEATURE_POSITIONAL)) {
+ // deactivate this feature only if it is available in the first place
+ featuresToDeactivate |= FEATURE_POSITIONAL;
+ }
+
+ if (featuresToDeactivate != FEATURE_NONE) {
+ uint32_t remainingFeatures = Global::get().pluginManager->deactivateFeaturesFor(plugin->getID(), featuresToDeactivate);
+
+ if (remainingFeatures != FEATURE_NONE) {
+ Global::get().l->log(Log::Warning, tr("Unable to deactivate all requested features for plugin \"%1\"").arg(plugin->getName()));
+ }
+ }
+ } else {
+ // loading failed
+ enable = false;
+ Global::get().l->log(Log::Warning, tr("Unable to load plugin \"%1\"").arg(plugin->getName()));
+ }
+ } else {
+ Global::get().pluginManager->unloadPlugin(plugin->getID());
+ }
+
+ QString pluginKey = QLatin1String(QCryptographicHash::hash(plugin->getFilePath().toUtf8(), QCryptographicHash::Sha1).toHex());
+ s.qhPluginSettings.insert(pluginKey, { plugin->getFilePath(), enable, positionalDataEnabled, keyboardMonitoringEnabled });
+ }
+ }
+}
+
+const_plugin_ptr_t PluginConfig::pluginForItem(QTreeWidgetItem *i) const {
+ if (i) {
+ return Global::get().pluginManager->getPlugin(i->data(0, Qt::UserRole).toUInt());
+ }
+
+ return nullptr;
+}
+
+void PluginConfig::on_qpbConfig_clicked() {
+ const_plugin_ptr_t plugin = pluginForItem(qtwPlugins->currentItem());
+
+ if (plugin) {
+ if (!plugin->showConfigDialog(this)) {
+ // if the plugin doesn't support showing such a dialog, we'll show a default one
+ QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no configure function."), QMessageBox::Ok, QMessageBox::NoButton);
+ }
+ }
+}
+
+void PluginConfig::on_qpbAbout_clicked() {
+ const_plugin_ptr_t plugin = pluginForItem(qtwPlugins->currentItem());
+
+ if (plugin) {
+ if (!plugin->showAboutDialog(this)) {
+ // if the plugin doesn't support showing such a dialog, we'll show a default one
+ QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no about function."), QMessageBox::Ok, QMessageBox::NoButton);
+ }
+ }
+}
+
+void PluginConfig::on_qpbReload_clicked() {
+ Global::get().pluginManager->rescanPlugins();
+ refillPluginList();
+}
+
+void PluginConfig::on_qpbUnload_clicked() {
+ QTreeWidgetItem *currentItem = qtwPlugins->currentItem();
+ if (!currentItem) {
+ return;
+ }
+
+ const_plugin_ptr_t plugin = pluginForItem(currentItem);
+ if (!plugin) {
+ return;
+ }
+
+ if (Global::get().pluginManager->clearPlugin(plugin->getID())) {
+ // Plugin was successfully cleared
+ currentItem = qtwPlugins->takeTopLevelItem(qtwPlugins->indexOfTopLevelItem(currentItem));
+
+ delete currentItem;
+ } else {
+ qWarning("PluginConfig.cpp: Failed to delete unloaded plugin entry");
+ }
+}
+
+void PluginConfig::refillPluginList() {
+ qtwPlugins->clear();
+
+ // get plugins already sorted according to their name
+ const QVector<const_plugin_ptr_t > plugins = Global::get().pluginManager->getPlugins(true);
+
+ foreach(const_plugin_ptr_t currentPlugin, plugins) {
+ QTreeWidgetItem *i = new QTreeWidgetItem(qtwPlugins);
+ i->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
+ i->setCheckState(1, currentPlugin->isLoaded() ? Qt::Checked : Qt::Unchecked);
+
+ if (currentPlugin->getFeatures() & FEATURE_POSITIONAL) {
+ i->setCheckState(2, currentPlugin->isPositionalDataEnabled() ? Qt::Checked : Qt::Unchecked);
+ i->setToolTip(2, tr("Whether the positional audio feature of this plugin should be enabled"));
+ } else {
+ i->setToolTip(2, tr("This plugin does not provide support for positional audio"));
+ }
+
+ i->setCheckState(3, currentPlugin->isKeyboardMonitoringAllowed() ? Qt::Checked : Qt::Unchecked);
+ i->setToolTip(3, tr("Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus"));
+
+ i->setText(0, currentPlugin->getName());
+ i->setToolTip(0, currentPlugin->getDescription().toHtmlEscaped());
+ i->setToolTip(1, tr("Whether this plugin should be enabled"));
+ i->setData(0, Qt::UserRole, currentPlugin->getID());
+ }
+
+ qtwPlugins->setCurrentItem(qtwPlugins->topLevelItem(0));
+ on_qtwPlugins_currentItemChanged(qtwPlugins->topLevelItem(0), NULL);
+}
+
+void PluginConfig::on_qtwPlugins_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *) {
+ const_plugin_ptr_t plugin = pluginForItem(current);
+
+ if (plugin) {
+ qpbAbout->setEnabled(plugin->providesAboutDialog());
+
+ qpbConfig->setEnabled(plugin->providesConfigDialog());
+
+ qpbUnload->setEnabled(true);
+ } else {
+ qpbAbout->setEnabled(false);
+ qpbConfig->setEnabled(false);
+ qpbUnload->setEnabled(false);
+ }
+}
diff --git a/src/mumble/PluginConfig.h b/src/mumble/PluginConfig.h
new file mode 100644
index 000000000..cab9ce7b3
--- /dev/null
+++ b/src/mumble/PluginConfig.h
@@ -0,0 +1,66 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGINS_H_
+#define MUMBLE_MUMBLE_PLUGINS_H_
+
+#include "ConfigDialog.h"
+#include "ui_PluginConfig.h"
+#include "Plugin.h"
+
+#include <QtCore/QObject>
+#include <QtCore/QMutex>
+#include <QtCore/QReadWriteLock>
+
+struct PluginInfo;
+
+class PluginConfig : public ConfigWidget, public Ui::PluginConfig {
+ private:
+ Q_OBJECT
+ Q_DISABLE_COPY(PluginConfig)
+ protected:
+ /// Clears and (re-) populates the plugin list in the UI with the currently available plugins
+ void refillPluginList();
+ /// @param item The QTreeWidgetItem to retrieve the plugin for
+ /// @returns The plugin corresponding to the provided item
+ const_plugin_ptr_t pluginForItem(QTreeWidgetItem *item) const;
+ public:
+ /// The unique name of this ConfigWidget
+ static const QString name;
+ /// Constructor
+ ///
+ /// @param st The settings object to work on
+ PluginConfig(Settings &st);
+ /// @returns The title of this widget
+ virtual QString title() const Q_DECL_OVERRIDE;
+ /// @returns The name of this ConfigWidget
+ const QString &getName() const Q_DECL_OVERRIDE;
+ /// @returns The icon for this widget
+ virtual QIcon icon() const Q_DECL_OVERRIDE;
+ public slots:
+ /// Saves the current configuration to the respective settings object
+ void save() const Q_DECL_OVERRIDE;
+ /// Loads the transmit-position from the provided settings object
+ ///
+ /// @param The setting sobject to read from
+ void load(const Settings &r) Q_DECL_OVERRIDE;
+ /// Slot triggered when the install-button in the UI has been clicked
+ void on_qpbInstallPlugin_clicked();
+ /// Slot triggered when the config-button in the UI has been clicked
+ void on_qpbConfig_clicked();
+ /// Slot triggered when the about-button in the UI has been clicked
+ void on_qpbAbout_clicked();
+ /// Slot triggered when the reload-button in the UI has been clicked
+ void on_qpbReload_clicked();
+ /// Slot triggered when the unload-button in the UI has been clicked
+ void on_qpbUnload_clicked();
+ /// Slot triggered when the selection in the plugin list hast changed
+ ///
+ /// @param current The currently selected item
+ /// @param old The previously selected item (if applicable - otherwise NULL/nullptr)
+ void on_qtwPlugins_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *old);
+};
+
+#endif
diff --git a/src/mumble/Plugins.ui b/src/mumble/PluginConfig.ui
index b634d3f0b..fff242cac 100644
--- a/src/mumble/Plugins.ui
+++ b/src/mumble/PluginConfig.ui
@@ -6,14 +6,14 @@
<rect>
<x>0</x>
<y>0</y>
- <width>321</width>
- <height>235</height>
+ <width>570</width>
+ <height>289</height>
</rect>
</property>
<property name="windowTitle">
<string>Plugins</string>
</property>
- <layout class="QVBoxLayout">
+ <layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="qgbOptions">
<property name="title">
@@ -53,9 +53,6 @@
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
- <attribute name="headerStretchLastSection">
- <bool>false</bool>
- </attribute>
<column>
<property name="text">
<string>Name</string>
@@ -63,7 +60,17 @@
</column>
<column>
<property name="text">
- <string>Enabled</string>
+ <string>Enable</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>PA</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>KeyEvents</string>
</property>
</column>
</widget>
@@ -84,6 +91,16 @@
</widget>
</item>
<item>
+ <widget class="QPushButton" name="qpbInstallPlugin">
+ <property name="toolTip">
+ <string>Install a plugin from a local file</string>
+ </property>
+ <property name="text">
+ <string>Install plugin...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -122,6 +139,16 @@
</property>
</widget>
</item>
+ <item>
+ <widget class="QPushButton" name="qpbUnload">
+ <property name="toolTip">
+ <string>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</string>
+ </property>
+ <property name="text">
+ <string>Unload</string>
+ </property>
+ </widget>
+ </item>
</layout>
</item>
</layout>
diff --git a/src/mumble/PluginInstaller.cpp b/src/mumble/PluginInstaller.cpp
new file mode 100644
index 000000000..41494d64e
--- /dev/null
+++ b/src/mumble/PluginInstaller.cpp
@@ -0,0 +1,200 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "PluginInstaller.h"
+#include "Global.h"
+
+#include <QtCore/QString>
+#include <QtCore/QException>
+#include <QtCore/QObject>
+#include <QtCore/QStringList>
+#include <QtCore/QDir>
+
+#include <QtGui/QIcon>
+
+#include <exception>
+#include <string>
+#include <fstream>
+
+#include <Poco/Zip/ZipArchive.h>
+#include <Poco/Zip/ZipStream.h>
+#include <Poco/StreamCopier.h>
+#include <Poco/Exception.h>
+
+PluginInstallException::PluginInstallException(const QString& msg)
+ : m_msg(msg) {
+}
+
+QString PluginInstallException::getMessage() const {
+ return m_msg;
+}
+
+const QString PluginInstaller::pluginFileExtension = QLatin1String("mumble_plugin");
+
+bool PluginInstaller::canBePluginFile(const QFileInfo& fileInfo) noexcept {
+ if (!fileInfo.isFile()) {
+ // A plugin file has to be a file (obviously)
+ return false;
+ }
+
+ if (fileInfo.suffix().compare(PluginInstaller::pluginFileExtension, Qt::CaseInsensitive) == 0
+ || fileInfo.suffix().compare(QLatin1String("zip"), Qt::CaseInsensitive) == 0) {
+ // A plugin file has either the extension given in PluginInstaller::pluginFileExtension or zip
+ return true;
+ }
+
+ // We might also accept a shared library directly
+ return QLibrary::isLibrary(fileInfo.fileName());
+}
+
+PluginInstaller::PluginInstaller(const QFileInfo& fileInfo, QWidget *p)
+ : QDialog(p),
+ m_pluginArchive(fileInfo),
+ m_plugin(nullptr),
+ m_pluginSource(),
+ m_pluginDestination(),
+ m_copyPlugin(false) {
+ setupUi(this);
+
+ setWindowIcon(QIcon(QLatin1String("skin:mumble.svg")));
+
+ QObject::connect(qpbYes, &QPushButton::clicked, this, &PluginInstaller::on_qpbYesClicked);
+ QObject::connect(qpbNo, &QPushButton::clicked, this, &PluginInstaller::on_qpbNoClicked);
+
+ init();
+}
+
+PluginInstaller::~PluginInstaller() {
+ if (m_plugin) {
+ delete m_plugin;
+ }
+}
+
+void PluginInstaller::init() {
+ if (!PluginInstaller::canBePluginFile(m_pluginArchive)) {
+ throw PluginInstallException(tr("The file \"%1\" is not a valid plugin file!").arg(m_pluginArchive.fileName()));
+ }
+
+ if (QLibrary::isLibrary(m_pluginArchive.fileName())) {
+ // For a library the fileInfo provided is already the actual plugin library
+ m_pluginSource = m_pluginArchive;
+
+ m_copyPlugin = true;
+ } else {
+ // We have been provided with a zip-file
+ try {
+ std::ifstream zipInput(m_pluginArchive.filePath().toStdString());
+ Poco::Zip::ZipArchive archive(zipInput);
+
+ // Iterate over all files in the archive to see which ones could be the correct plugin library
+ QString pluginName;
+ auto it = archive.fileInfoBegin();
+ while (it != archive.fileInfoEnd()) {
+ QString currentFileName = QString::fromStdString(it->first);
+ if (QLibrary::isLibrary(currentFileName)) {
+ if (!pluginName.isEmpty()) {
+ // There seem to be multiple plugins in here. That's not allowed
+ throw PluginInstallException(tr("Found more than one plugin library for the current OS in \"%1\" (\"%2\" and \"%3\")!").arg(
+ m_pluginArchive.fileName()).arg(pluginName).arg(currentFileName));
+ }
+
+ pluginName = currentFileName;
+ }
+
+ it++;
+ }
+
+ if (pluginName.isEmpty()) {
+ throw PluginInstallException(tr("Unable to find a plugin for the current OS in \"%1\"").arg(m_pluginArchive.fileName()));
+ }
+
+ // Unpack the plugin library into the tmp dir
+ // We don't have to create the directory structure as we're only interested in the library itself
+ QString tmpPluginPath = QDir::temp().filePath(QFileInfo(pluginName).fileName());
+ auto pluginIt = archive.findHeader(pluginName.toStdString());
+ zipInput.clear();
+ Poco::Zip::ZipInputStream zipin(zipInput, pluginIt->second);
+ std::ofstream out(tmpPluginPath.toStdString());
+ Poco::StreamCopier::copyStream(zipin, out);
+
+ m_pluginSource = QFileInfo(tmpPluginPath);
+ } catch(const Poco::Exception &e) {
+ // Something didn't work out during the Zip processing
+ throw PluginInstallException(QString::fromStdString(std::string("Failed to process zip archive: ") + e.message()));
+ }
+ }
+
+ QString pluginFileName = m_pluginSource.fileName();
+
+ // Try to load the plugin up to see if it is actually valid
+ try {
+ m_plugin = Plugin::createNew<Plugin>(m_pluginSource.absoluteFilePath());
+ } catch(const PluginError&) {
+ throw PluginInstallException(tr("Unable to load plugin \"%1\" - check the plugin interface!").arg(pluginFileName));
+ }
+
+ m_pluginDestination = QFileInfo(QString::fromLatin1("%1/%2").arg(getInstallDir()).arg(pluginFileName));
+
+
+ // Now that we located the plugin, it is time to fill in its details in the UI
+ qlName->setText(m_plugin->getName());
+
+ mumble_version_t pluginVersion = m_plugin->getVersion();
+ mumble_version_t usedAPIVersion = m_plugin->getAPIVersion();
+ qlVersion->setText(QString::fromLatin1("%1 (API %2)").arg(pluginVersion == VERSION_UNKNOWN ?
+ "Unknown" : static_cast<QString>(pluginVersion)).arg(
+ usedAPIVersion == VERSION_UNKNOWN ? "Unknown" : static_cast<QString>(usedAPIVersion)));
+
+ qlAuthor->setText(m_plugin->getAuthor());
+
+ qlDescription->setText(m_plugin->getDescription());
+}
+
+void PluginInstaller::install() const {
+ if (!m_plugin) {
+ // This function shouldn't even be called, if the plugin object has not been created...
+ throw PluginInstallException(QLatin1String("[INTERNAL ERROR]: Trying to install an invalid plugin"));
+ }
+
+ if (m_pluginSource == m_pluginDestination) {
+ // Apparently the plugin is already installed
+ return;
+ }
+
+ if (m_pluginDestination.exists()) {
+ // Delete old version first
+ if (!QFile(m_pluginDestination.absoluteFilePath()).remove()) {
+ throw PluginInstallException(tr("Unable to delete old plugin at \"%1\"").arg(m_pluginDestination.absoluteFilePath()));
+ }
+ }
+
+ if (m_copyPlugin) {
+ if (!QFile(m_pluginSource.absoluteFilePath()).copy(m_pluginDestination.absoluteFilePath())) {
+ throw PluginInstallException(tr("Unable to copy plugin library from \"%1\" to \"%2\"").arg(m_pluginSource.absoluteFilePath()).arg(
+ m_pluginDestination.absoluteFilePath()));
+ }
+ } else {
+ // Move the plugin into the respective dir
+ if (!QFile(m_pluginSource.absoluteFilePath()).rename(m_pluginDestination.absoluteFilePath())) {
+ throw PluginInstallException(tr("Unable to move plugin library to \"%1\"").arg(m_pluginDestination.absoluteFilePath()));
+ }
+ }
+}
+
+QString PluginInstaller::getInstallDir() {
+ // Get the path to the plugin-dir in "user-land" (aka: the user definitely has write access to this
+ // location).
+ return Global::get().qdBasePath.absolutePath() + QLatin1String("/Plugins");
+}
+
+void PluginInstaller::on_qpbYesClicked() {
+ install();
+
+ accept();
+}
+
+void PluginInstaller::on_qpbNoClicked() {
+ close();
+}
diff --git a/src/mumble/PluginInstaller.h b/src/mumble/PluginInstaller.h
new file mode 100644
index 000000000..d8df3c303
--- /dev/null
+++ b/src/mumble/PluginInstaller.h
@@ -0,0 +1,84 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGININSTALLER_H_
+#define MUMBLE_MUMBLE_PLUGININSTALLER_H_
+
+
+#include <QtCore/QFileInfo>
+#include <QtCore/QException>
+
+#include "Plugin.h"
+
+#include "ui_PluginInstaller.h"
+
+/// An exception thrown by the PluginInstaller
+class PluginInstallException : public QException {
+ protected:
+ /// The exception's message
+ QString m_msg;
+ public:
+ /// @param msg The message stating why this exception has been thrown
+ PluginInstallException(const QString& msg);
+
+ /// @returns This exception's message
+ QString getMessage() const;
+};
+
+/// The PluginInstaller can be used to install plugins into Mumble. It verifies that the respective
+/// plugin is functional and will automatiacally copy/move the plugin library to the respective
+/// directory on the FileSystem.
+class PluginInstaller : public QDialog, public Ui::PluginInstaller {
+ private:
+ Q_OBJECT;
+ Q_DISABLE_COPY(PluginInstaller);
+ protected:
+ /// The file the installer has been invoked on
+ QFileInfo m_pluginArchive;
+ /// A pointer to the plugin instance created from the plugin library that shall be installed
+ Plugin *m_plugin;
+ /// The actual plugin library file
+ QFileInfo m_pluginSource;
+ /// The destinaton file to which the plugin library shall be copied
+ QFileInfo m_pluginDestination;
+ /// A flag indicating that the plugin library shall be copied instead of moved in order
+ /// to install it.
+ bool m_copyPlugin;
+
+ /// Initializes this installer by processing the provided plugin source and filling all
+ /// internal fields. This function is called from the constructor.
+ ///
+ /// @throws PluginInstallException If something isn't right or goes wrong
+ void init();
+ public:
+ /// The "special" file-extension associated with Mumble plugins
+ static const QString pluginFileExtension;
+
+ /// A helper function checking whether the provided file could be a plugin source
+ ///
+ /// @param fileInfo The file to check
+ /// @returns Whether the provided file could (!) be a plugin source
+ static bool canBePluginFile(const QFileInfo& fileInfo) noexcept;
+
+ /// @param fileInfo The plugin source to process
+ ///
+ /// @throws PluginInstallException If something isn't right or goes wrong
+ PluginInstaller(const QFileInfo& fileInfo, QWidget *p = nullptr);
+ /// Destructor
+ ~PluginInstaller();
+
+ /// Performs the actual installation (moving/copying of the library) of the plugin
+ void install() const;
+
+ static QString getInstallDir();
+
+ public slots:
+ /// Slot called when the user clicks the yes button
+ void on_qpbYesClicked();
+ /// Slot called when the user clicks the no button
+ void on_qpbNoClicked();
+};
+
+#endif // MUMBLE_MUMBLE_PLUGININSTALLER_H_
diff --git a/src/mumble/PluginInstaller.ui b/src/mumble/PluginInstaller.ui
new file mode 100644
index 000000000..39f575dea
--- /dev/null
+++ b/src/mumble/PluginInstaller.ui
@@ -0,0 +1,243 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PluginInstaller</class>
+ <widget class="QDialog" name="PluginInstaller">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>360</width>
+ <height>332</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>PluginInstaller</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="modal">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="qlPrompt">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="text">
+ <string>You are about to install the plugin listed below. Do you wish to proceed?</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="qwPluginInfo">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>348</width>
+ <height>208</height>
+ </rect>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="labelAlignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="verticalSpacing">
+ <number>12</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="qlName_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="qlName">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="qlVersion_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="qlVersion">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="qlAuthor_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="qlAuthor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="qlDescription_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="qlDescription">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QFrame" name="frame">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="bottomMargin">
+ <number>6</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="qpbNo">
+ <property name="minimumSize">
+ <size>
+ <width>80</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>&amp;No</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="qpbYes">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>80</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>&amp;Yes</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/mumble/PluginManager.cpp b/src/mumble/PluginManager.cpp
new file mode 100644
index 000000000..817eb69be
--- /dev/null
+++ b/src/mumble/PluginManager.cpp
@@ -0,0 +1,933 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include <limits>
+
+#include "PluginManager.h"
+#include "LegacyPlugin.h"
+#include <QReadLocker>
+#include <QWriteLocker>
+#include <QReadLocker>
+#include <QDir>
+#include <QFileInfoList>
+#include <QFileInfo>
+#include <QVector>
+#include <QByteArray>
+#include <QChar>
+#include <QMutexLocker>
+#include <QHashIterator>
+#include <QKeyEvent>
+#include <QTimer>
+
+#include "ManualPlugin.h"
+#include "Log.h"
+#include "PluginInstaller.h"
+#include "ProcessResolver.h"
+#include "ServerHandler.h"
+#include "PluginUpdater.h"
+#include "API.h"
+#include "Global.h"
+
+#include <memory>
+
+#ifdef Q_OS_WIN
+ #include <tlhelp32.h>
+ #include <string>
+#endif
+
+#ifdef Q_OS_LINUX
+ #include <QtCore/QStringList>
+#endif
+
+PluginManager::PluginManager(QSet<QString> *additionalSearchPaths, QObject *p)
+ : QObject(p),
+ m_pluginCollectionLock(QReadWriteLock::NonRecursive),
+ m_pluginHashMap(),
+ m_positionalData(),
+ m_positionalDataCheckTimer(),
+ m_sentDataMutex(),
+ m_sentData(),
+ m_activePosDataPluginLock(QReadWriteLock::NonRecursive),
+ m_activePositionalDataPlugin(),
+ m_updater() {
+
+ // Setup search-paths
+ if (additionalSearchPaths) {
+ for (const auto &currentPath : *additionalSearchPaths) {
+ m_pluginSearchPaths.insert(currentPath);
+ }
+ }
+
+#ifdef Q_OS_MAC
+ // Path to plugins inside AppBundle
+ m_pluginSearchPaths.insert(QString::fromLatin1("%1/../Plugins").arg(qApp->applicationDirPath()));
+#endif
+
+#ifdef MUMBLE_PLUGIN_PATH
+ // Path to where plugins are/will be installed on the system
+ m_pluginSearchPaths.insert(QString::fromLatin1(MUMTEXT(MUMBLE_PLUGIN_PATH)));
+#endif
+
+ // Path to "plugins" dir right next to the executable's location. This is the case for when Mumble
+ // is run after compilation without having installed it anywhere special
+ m_pluginSearchPaths.insert(QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath()));
+
+ // Path to where the plugin installer will write plugins
+ m_pluginSearchPaths.insert(PluginInstaller::getInstallDir());
+
+#ifdef Q_OS_WIN
+ // According to MS KB Q131065, we need this to OpenProcess()
+
+ m_hToken = nullptr;
+
+ if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken)) {
+ if (GetLastError() == ERROR_NO_TOKEN) {
+ ImpersonateSelf(SecurityImpersonation);
+ OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken);
+ }
+ }
+
+ TOKEN_PRIVILEGES tp;
+ LUID luid;
+ m_cbPrevious=sizeof(TOKEN_PRIVILEGES);
+
+ LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
+
+ tp.PrivilegeCount = 1;
+ tp.Privileges[0].Luid = luid;
+ tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
+
+ AdjustTokenPrivileges(m_hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &m_tpPrevious, &m_cbPrevious);
+#endif
+
+ // Synchronize the positional data in a regular interval
+ // By making this the parent of the created timer, we don't have to delete it explicitly
+ QTimer *serverSyncTimer = new QTimer(this);
+ QObject::connect(serverSyncTimer, &QTimer::timeout, this, &PluginManager::on_syncPositionalData);
+ serverSyncTimer->start(POSITIONAL_SERVER_SYNC_INTERVAL);
+
+ // Install this manager as a global eventFilter in order to get notified about all keypresses
+ if (QCoreApplication::instance()) {
+ QCoreApplication::instance()->installEventFilter(this);
+ }
+
+ // Set up the timer for regularly checking for available positional data plugins
+ m_positionalDataCheckTimer.setInterval(POSITIONAL_DATA_CHECK_INTERVAL);
+ m_positionalDataCheckTimer.start();
+ QObject::connect(&m_positionalDataCheckTimer, &QTimer::timeout, this, &PluginManager::checkForAvailablePositionalDataPlugin);
+
+ QObject::connect(&m_updater, &PluginUpdater::updatesAvailable, this, &PluginManager::on_updatesAvailable);
+ QObject::connect(this, &PluginManager::keyEvent, this, &PluginManager::on_keyEvent);
+}
+
+PluginManager::~PluginManager() {
+ clearPlugins();
+
+#ifdef Q_OS_WIN
+ AdjustTokenPrivileges(m_hToken, FALSE, &m_tpPrevious, m_cbPrevious, NULL, NULL);
+ CloseHandle(m_hToken);
+#endif
+}
+
+/// Emits a log about a plugin with the given name having lost link (positional audio)
+///
+/// @param pluginName The name of the plugin that lost link
+void reportLostLink(const QString& pluginName) {
+ Global::get().l->log(Log::Information, PluginManager::tr("%1 lost link").arg(pluginName.toHtmlEscaped()));
+}
+
+bool PluginManager::eventFilter(QObject *target, QEvent *event) {
+ if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
+ static QVector<QKeyEvent*> processedEvents;
+
+ QKeyEvent *kEvent = static_cast<QKeyEvent *>(event);
+
+ // We have to keep track of which events we have processed already as
+ // the same event might be sent to multiple targets and since this is
+ // installed as a global event filter, we get notified about each of
+ // them. However we want to process each event only once.
+ if (!kEvent->isAutoRepeat() && !processedEvents.contains(kEvent)) {
+ // Fire event
+ emit keyEvent(kEvent->key(), kEvent->modifiers(), kEvent->type() == QEvent::KeyPress);
+
+ processedEvents << kEvent;
+
+ if (processedEvents.size() == 1) {
+ // Make sure to clear the list of processed events after each iteration
+ // of the event loop (we don't want to let the vector grow to infinity
+ // over time. Firing the timer only when the size of processedEvents is
+ // exactly 1, we avoid adding multiple timers in a single iteration.
+ QTimer::singleShot(0, []() { processedEvents.clear(); });
+ }
+ }
+
+ }
+
+ // standard event processing
+ return QObject::eventFilter(target, event);
+}
+
+void PluginManager::unloadPlugins() const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ auto it = m_pluginHashMap.begin();
+
+ while (it != m_pluginHashMap.end()) {
+ unloadPlugin(*it.value());
+ it++;
+ }
+}
+
+void PluginManager::clearPlugins() {
+ // Unload plugins so that they aren't implicitly unloaded once they go out of scope after having been
+ // removed from the pluginHashMap.
+ // This could lead to one of the plugins making an API call in its shutdown function which then would try
+ // to verify the plugin's ID. For that it'll ask this PluginManager for a plugin with that ID. To check
+ // that it will have to aquire a read-lock for the pluginHashMap which is impossible after we aquire the
+ // write-lock in this function leading to a deadlock.
+ unloadPlugins();
+
+ QWriteLocker lock(&m_pluginCollectionLock);
+
+ // Clear the list itself
+ m_pluginHashMap.clear();
+}
+
+bool PluginManager::selectActivePositionalDataPlugin() {
+ QReadLocker pluginLock(&m_pluginCollectionLock);
+ QWriteLocker activePluginLock(&m_activePosDataPluginLock);
+
+ if (!Global::get().s.bTransmitPosition) {
+ // According to the settings the position shall not be transmitted meaning that we don't have to select any plugin
+ // for positional data
+ m_activePositionalDataPlugin = nullptr;
+
+ return false;
+ }
+
+ ProcessResolver procRes(true);
+
+ auto it = m_pluginHashMap.begin();
+
+ // We assume that there is only one (enabled) plugin for the currently played game so we don't have to remember
+ // which plugin was active last
+ while (it != m_pluginHashMap.end()) {
+ plugin_ptr_t currentPlugin = it.value();
+
+ if (currentPlugin->isPositionalDataEnabled() && currentPlugin->isLoaded()) {
+ switch(currentPlugin->initPositionalData(procRes.getProcessNames().data(),
+ procRes.getProcessPIDs().data(), procRes.amountOfProcesses())) {
+ case PDEC_OK:
+ // the plugin is ready to provide positional data
+ m_activePositionalDataPlugin = currentPlugin;
+
+ Global::get().l->log(Log::Information, tr("%1 linked").arg(currentPlugin->getName().toHtmlEscaped()));
+
+ return true;
+
+ case PDEC_ERROR_PERM:
+ // the plugin encountered a permanent error -> disable it
+ Global::get().l->log(Log::Warning, tr(
+ "Plugin \"%1\" encountered a permanent error in positional data gathering").arg(currentPlugin->getName()));
+
+ currentPlugin->enablePositionalData(false);
+ break;
+
+ case PDEC_ERROR_TEMP:
+ //The plugin encountered a temporary error -> skip it for now (that is: do nothing)
+ break;
+ }
+ }
+
+ it++;
+ }
+
+ m_activePositionalDataPlugin = nullptr;
+
+ return false;
+}
+
+#define LOG_FOUND(plugin, path, legacyStr) qDebug("Found %splugin '%s' at \"%s\"", legacyStr, qUtf8Printable(plugin->getName()), qUtf8Printable(path));\
+ qDebug() << "Its description:" << qUtf8Printable(plugin->getDescription())
+#define LOG_FOUND_PLUGIN(plugin, path) LOG_FOUND(plugin, path, "")
+#define LOG_FOUND_LEGACY_PLUGIN(plugin, path) LOG_FOUND(plugin, path, "legacy ")
+#define LOG_FOUND_BUILTIN(plugin) LOG_FOUND(plugin, QString::fromLatin1("<builtin>"), "built-in ")
+void PluginManager::rescanPlugins() {
+ clearPlugins();
+
+ {
+ QWriteLocker lock(&m_pluginCollectionLock);
+
+ // iterate over all files in the respective directories and try to construct a plugin from them
+ for (const auto &currentPath : m_pluginSearchPaths) {
+ QFileInfoList currentList = QDir(currentPath).entryInfoList();
+
+ for (int k=0; k<currentList.size(); k++) {
+ QFileInfo currentInfo = currentList[k];
+
+ if (!QLibrary::isLibrary(currentInfo.absoluteFilePath())) {
+ // consider only files that actually could be libraries
+ continue;
+ }
+
+ try {
+ plugin_ptr_t p(Plugin::createNew<Plugin>(currentInfo.absoluteFilePath()));
+
+#ifdef MUMBLE_PLUGIN_DEBUG
+ LOG_FOUND_PLUGIN(p, currentInfo.absoluteFilePath());
+#endif
+
+ // if this code block is reached, the plugin was instantiated successfully so we can add it to the map
+ m_pluginHashMap.insert(p->getID(), p);
+ } catch(const PluginError& e) {
+ Q_UNUSED(e);
+ // If an exception is thrown, this library does not represent a proper plugin
+ // Check if it might be a legacy plugin instead
+ try {
+ legacy_plugin_ptr_t lp(Plugin::createNew<LegacyPlugin>(currentInfo.absoluteFilePath()));
+
+#ifdef MUMBLE_PLUGIN_DEBUG
+ LOG_FOUND_LEGACY_PLUGIN(lp, currentInfo.absoluteFilePath());
+#endif
+ m_pluginHashMap.insert(lp->getID(), lp);
+ } catch(const PluginError& e) {
+ Q_UNUSED(e);
+
+ // At the time this function is running the MainWindow is not necessarily created yet, so we can't use
+ // the normal Log::log function
+ Log::logOrDefer(Log::Warning,
+ tr("Non-plugin found in plugin directory: \"%1\"").arg(currentInfo.absoluteFilePath()));
+ }
+ }
+ }
+ }
+
+ // handle built-in plugins
+#ifdef USE_MANUAL_PLUGIN
+ try {
+ std::shared_ptr<ManualPlugin> mp(Plugin::createNew<ManualPlugin>());
+
+ m_pluginHashMap.insert(mp->getID(), mp);
+#ifdef MUMBLE_PLUGIN_DEBUG
+ LOG_FOUND_BUILTIN(mp);
+#endif
+ } catch(const PluginError& e) {
+ // At the time this function is running the MainWindow is not necessarily created yet, so we can't use
+ // the normal Log::log function
+ Log::logOrDefer(Log::Warning, tr("Failed at loading manual plugin: %1").arg(QString::fromUtf8(e.what())));
+ }
+#endif
+ }
+
+ QReadLocker readLock(&m_pluginCollectionLock);
+
+ // load plugins based on settings
+ // iterate over all plugins that have saved settings
+ auto it = Global::get().s.qhPluginSettings.constBegin();
+ while (it != Global::get().s.qhPluginSettings.constEnd()) {
+ // for this we need a way to get a plugin based on the filepath
+ const QString pluginKey = it.key();
+ const PluginSetting setting = it.value();
+
+ // iterate over all loaded plugins to see if the current setting is applicable
+ auto pluginIt = m_pluginHashMap.begin();
+ while (pluginIt != m_pluginHashMap.end()) {
+ plugin_ptr_t plugin = pluginIt.value();
+ QString pluginHash = QLatin1String(QCryptographicHash::hash(plugin->getFilePath().toUtf8(), QCryptographicHash::Sha1).toHex());
+ if (pluginKey == pluginHash) {
+ if (setting.enabled) {
+ loadPlugin(plugin->getID());
+
+ const uint32_t features = plugin->getFeatures();
+
+ if (!setting.positionalDataEnabled && (features & FEATURE_POSITIONAL)) {
+ // try to deactivate the feature if the setting says so
+ plugin->deactivateFeatures(FEATURE_POSITIONAL);
+ }
+ }
+
+ // positional data is a special feature that has to be enabled/disabled in the Plugin wrapper class
+ // additionally to telling the plugin library that the feature shall be deactivated
+ plugin->enablePositionalData(setting.positionalDataEnabled);
+
+ plugin->allowKeyboardMonitoring(setting.allowKeyboardMonitoring);
+
+ break;
+ }
+
+ pluginIt++;
+ }
+
+ it++;
+ }
+}
+
+const_plugin_ptr_t PluginManager::getPlugin(plugin_id_t pluginID) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ return m_pluginHashMap.value(pluginID);
+}
+
+void PluginManager::checkForPluginUpdates() {
+ m_updater.checkForUpdates();
+}
+
+bool PluginManager::fetchPositionalData() {
+ if (Global::get().bPosTest) {
+ // This is for testing-purposes only so the "fetched" position doesn't have any real meaning
+ m_positionalData.reset();
+
+ m_positionalData.m_playerDir.z = 1.0f;
+ m_positionalData.m_playerAxis.y = 1.0f;
+ m_positionalData.m_cameraDir.z = 1.0f;
+ m_positionalData.m_cameraAxis.y = 1.0f;
+
+ return true;
+ }
+
+ QReadLocker activePluginLock(&m_activePosDataPluginLock);
+
+ if (!m_activePositionalDataPlugin) {
+ // It appears as if there is currently no plugin capable of delivering positional audio
+ // Set positional data to zero-values
+ m_positionalData.reset();
+
+ return false;
+ }
+
+ QWriteLocker posDataLock(&m_positionalData.m_lock);
+
+ bool retStatus = m_activePositionalDataPlugin->fetchPositionalData(m_positionalData.m_playerPos, m_positionalData.m_playerDir,
+ m_positionalData.m_playerAxis, m_positionalData.m_cameraPos, m_positionalData.m_cameraDir, m_positionalData.m_cameraAxis,
+ m_positionalData.m_context, m_positionalData.m_identity);
+
+ // Add the plugin's name to the context as well to prevent name-clashes between plugins
+ if (!m_positionalData.m_context.isEmpty()) {
+ m_positionalData.m_context = m_activePositionalDataPlugin->getName() + QChar::Null + m_positionalData.m_context;
+ }
+
+ if (!retStatus) {
+ // Shut the currently active plugin down and set a new one (if available)
+ m_activePositionalDataPlugin->shutdownPositionalData();
+
+ reportLostLink(m_activePositionalDataPlugin->getName());
+
+ // unlock the read-lock in order to allow selectActivePositionaldataPlugin to gain a write-lock
+ activePluginLock.unlock();
+
+ selectActivePositionalDataPlugin();
+ } else {
+ // If the return-status doesn't indicate an error, we can assume that positional data is available
+ // The remaining problematic case is, if the player is exactly at position (0,0,0) as this is used as an indicator for the
+ // absence of positional data in the mix() function in AudioOutput.cpp
+ // Thus we have to make sure that this position is never set if positional data is actually available.
+ // We solve this problem by shifting the player a minimal amount on the z-axis
+ if (m_positionalData.m_playerPos == Position3D(0.0f, 0.0f, 0.0f)) {
+ m_positionalData.m_playerPos = {0.0f, 0.0f, std::numeric_limits<float>::min()};
+ }
+ if (m_positionalData.m_cameraPos == Position3D(0.0f, 0.0f, 0.0f)) {
+ m_positionalData.m_cameraPos = {0.0f, 0.0f, std::numeric_limits<float>::min()};
+ }
+ }
+
+ return retStatus;
+}
+
+void PluginManager::unlinkPositionalData() {
+ QWriteLocker lock(&m_activePosDataPluginLock);
+
+ if (m_activePositionalDataPlugin) {
+ m_activePositionalDataPlugin->shutdownPositionalData();
+
+ reportLostLink(m_activePositionalDataPlugin->getName());
+
+ // Set the pointer to nullptr
+ m_activePositionalDataPlugin = nullptr;
+ }
+}
+
+bool PluginManager::isPositionalDataAvailable() const {
+ QReadLocker lock(&m_activePosDataPluginLock);
+
+ return m_activePositionalDataPlugin != nullptr;
+}
+
+const PositionalData& PluginManager::getPositionalData() const {
+ return m_positionalData;
+}
+
+void PluginManager::enablePositionalDataFor(plugin_id_t pluginID, bool enable) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ plugin->enablePositionalData(enable);
+ }
+}
+
+const QVector<const_plugin_ptr_t > PluginManager::getPlugins(bool sorted) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ QVector<const_plugin_ptr_t> pluginList;
+
+ auto it = m_pluginHashMap.constBegin();
+ if (sorted) {
+ QList<plugin_id_t> ids = m_pluginHashMap.keys();
+
+ // sort keys so that the corresponding Plugins are in alphabetical order based on their name
+ std::sort(ids.begin(), ids.end(), [this](plugin_id_t first, plugin_id_t second) {
+ return QString::compare(m_pluginHashMap.value(first)->getName(), m_pluginHashMap.value(second)->getName(),
+ Qt::CaseInsensitive) <= 0;
+ });
+
+ foreach(plugin_id_t currentID, ids) {
+ pluginList.append(m_pluginHashMap.value(currentID));
+ }
+ } else {
+ while (it != m_pluginHashMap.constEnd()) {
+ pluginList.append(it.value());
+
+ it++;
+ }
+ }
+
+ return pluginList;
+}
+
+bool PluginManager::loadPlugin(plugin_id_t pluginID) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ if (plugin->isLoaded()) {
+ // Don't attempt to load a plugin if it already is loaded.
+ // This can happen if the user clicks the apply button in the settings
+ // before hitting ok.
+ return true;
+ }
+
+ return plugin->init() == STATUS_OK;
+ }
+
+ return false;
+}
+
+void PluginManager::unloadPlugin(plugin_id_t pluginID) const {
+ plugin_ptr_t plugin;
+ {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin = m_pluginHashMap.value(pluginID);
+ }
+
+ if (plugin) {
+ unloadPlugin(*plugin);
+ }
+}
+
+void PluginManager::unloadPlugin(Plugin &plugin) const {
+ if (plugin.isLoaded()) {
+ // Only shut down loaded plugins
+ plugin.shutdown();
+ }
+}
+
+bool PluginManager::clearPlugin(plugin_id_t pluginID) {
+ // We have to unload the plugin before we take the write lock. The reasoning being that if
+ // the plugin makes an API call in its shutdown callback, that'll lead to this manager being
+ // asked for whether a plugin with such an ID exists. The function performing this check will
+ // take a read lock which is not possible if we hold a write lock here already (deadlock).
+ unloadPlugin(pluginID);
+
+ QWriteLocker lock(&m_pluginCollectionLock);
+
+ // Remove the plugin from the list of known plugins
+ plugin_ptr_t plugin = m_pluginHashMap.take(pluginID);
+
+ return plugin != nullptr;
+}
+
+uint32_t PluginManager::deactivateFeaturesFor(plugin_id_t pluginID, uint32_t features) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ return plugin->deactivateFeatures(features);
+ }
+
+ return FEATURE_NONE;
+}
+
+void PluginManager::allowKeyboardMonitoringFor(plugin_id_t pluginID, bool allow) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ return plugin->allowKeyboardMonitoring(allow);
+ }
+}
+
+bool PluginManager::pluginExists(plugin_id_t pluginID) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ return m_pluginHashMap.contains(pluginID);
+}
+
+void PluginManager::foreachPlugin(std::function<void(Plugin&)> pluginProcessor) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ auto it = m_pluginHashMap.constBegin();
+
+ while (it != m_pluginHashMap.constEnd()) {
+ pluginProcessor(*it.value());
+
+ it++;
+ }
+}
+
+void PluginManager::on_serverConnected() const {
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug("PluginManager: Connected to a server with connection ID %d", connectionID);
+#endif
+
+ foreachPlugin([connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onServerConnected(connectionID);
+ }
+ });
+}
+
+void PluginManager::on_serverDisconnected() const {
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug("PluginManager: Disconnected from a server with connection ID %d", connectionID);
+#endif
+
+ foreachPlugin([connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onServerDisconnected(connectionID);
+ }
+ });
+}
+
+void PluginManager::on_channelEntered(const Channel *newChannel, const Channel *prevChannel, const User *user) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: User" << user->qsName << "entered channel" << newChannel->qsName << "- ID:" << newChannel->iId;
+#endif
+
+ if (!Global::get().sh) {
+ // if there is no server-handler, there is no (real) channel to enter
+ return;
+ }
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([user, newChannel, prevChannel, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelEntered(connectionID, user->uiSession, prevChannel ? prevChannel->iId : -1, newChannel->iId);
+ }
+ });
+}
+
+void PluginManager::on_channelExited(const Channel *channel, const User *user) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: User" << user->qsName << "left channel" << channel->qsName << "- ID:" << channel->iId;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([user, channel, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelExited(connectionID, user->uiSession, channel->iId);
+ }
+ });
+}
+
+QString getTalkingStateStr(Settings::TalkState ts) {
+ switch(ts) {
+ case Settings::TalkState::Passive:
+ return QString::fromLatin1("Passive");
+ case Settings::TalkState::Talking:
+ return QString::fromLatin1("Talking");
+ case Settings::TalkState::Whispering:
+ return QString::fromLatin1("Whispering");
+ case Settings::TalkState::Shouting:
+ return QString::fromLatin1("Shouting");
+ case Settings::TalkState::MutedTalking:
+ return QString::fromLatin1("MutedTalking");
+ }
+
+ return QString::fromLatin1("Unknown");
+}
+
+void PluginManager::on_userTalkingStateChanged() const {
+ const ClientUser *user = qobject_cast<ClientUser*>(QObject::sender());
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ if (user) {
+ qDebug() << "PluginManager: User" << user->qsName << "changed talking state to" << getTalkingStateStr(user->tsState);
+ } else {
+ qCritical() << "PluginManager: Unable to identify ClientUser";
+ }
+#endif
+
+ if (user) {
+ // Convert Mumble's talking state to the TalkingState used in the API
+ mumble_talking_state_t ts = INVALID;
+
+ switch(user->tsState) {
+ case Settings::TalkState::Passive:
+ ts = PASSIVE;
+ break;
+ case Settings::TalkState::Talking:
+ ts = TALKING;
+ break;
+ case Settings::TalkState::Whispering:
+ ts = WHISPERING;
+ break;
+ case Settings::TalkState::Shouting:
+ ts = SHOUTING;
+ break;
+ case Settings::TalkState::MutedTalking:
+ ts = TALKING_MUTED;
+ break;
+ }
+
+ if (ts == INVALID) {
+ qWarning("PluginManager.cpp: Invalid talking state encountered");
+ // An error occured
+ return;
+ }
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([user, ts, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserTalkingStateChanged(connectionID, user->uiSession, ts);
+ }
+ });
+ }
+}
+
+void PluginManager::on_audioInput(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioInput with" << channelCount << "channels and" << sampleCount << "samples per channel. IsSpeech:" << isSpeech;
+#endif
+
+ foreachPlugin([inputPCM, sampleCount, channelCount, sampleRate, isSpeech](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onAudioInput(inputPCM, sampleCount, channelCount, sampleRate, isSpeech);
+ }
+ });
+}
+
+void PluginManager::on_audioSourceFetched(float* outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech, const ClientUser* user) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioSource with" << channelCount << "channels and" << sampleCount << "samples per channel fetched. IsSpeech:" << isSpeech;
+ if (user != nullptr) {
+ qDebug() << "Sender-ID:" << user->uiSession;
+ }
+#endif
+
+ foreachPlugin([outputPCM, sampleCount, channelCount, sampleRate, isSpeech, user](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onAudioSourceFetched(outputPCM, sampleCount, channelCount, sampleRate, isSpeech, user ? user->uiSession : -1);
+ }
+ });
+}
+
+void PluginManager::on_audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool *modifiedAudio) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioOutput with" << channelCount << "channels and" << sampleCount << "samples per channel";
+#endif
+ foreachPlugin([outputPCM, sampleCount, channelCount, sampleRate, modifiedAudio](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ if(plugin.onAudioOutputAboutToPlay(outputPCM, sampleCount, sampleRate, channelCount)) {
+ *modifiedAudio = true;
+ }
+ }
+ });
+}
+
+void PluginManager::on_receiveData(const ClientUser *sender, const uint8_t *data, size_t dataLength, const char *dataID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Data with ID" << dataID << "and length" << dataLength << "received. Sender-ID:" << sender->uiSession;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([sender, data, dataLength, dataID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onReceiveData(connectionID, sender->uiSession, data, dataLength, dataID);
+ }
+ });
+}
+
+void PluginManager::on_serverSynchronized() const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Server synchronized";
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onServerSynchronized(connectionID);
+ }
+ });
+}
+
+void PluginManager::on_userAdded(mumble_userid_t userID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Added user with ID" << userID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([userID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserAdded(connectionID, userID);
+ };
+ });
+}
+
+void PluginManager::on_userRemoved(mumble_userid_t userID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Removed user with ID" << userID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([userID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserRemoved(connectionID, userID);
+ };
+ });
+}
+
+void PluginManager::on_channelAdded(mumble_channelid_t channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Added channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelAdded(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_channelRemoved(mumble_channelid_t channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Removed channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelRemoved(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_channelRenamed(int channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Renamed channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelRenamed(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Key event detected: keyCode =" << key << "modifiers:"
+ << modifiers << "isPress =" << isPress;
+#else
+ Q_UNUSED(modifiers);
+#endif
+
+ // Convert from Qt encoding to our own encoding
+ mumble_keycode_t keyCode = API::qtKeyCodeToAPIKeyCode(key);
+
+ foreachPlugin([keyCode, isPress](Plugin &plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onKeyEvent(keyCode, isPress);
+ }
+ });
+}
+
+void PluginManager::on_syncPositionalData() {
+ // fetch positional data
+ if (fetchPositionalData()) {
+ // Sync the gathered data (context + identity) with the server
+ if (!Global::get().uiSession) {
+ // For some reason the local session ID is not set -> clear all data sent to the server in order to gurantee
+ // a re-send once the session is restored and there is data available
+ QMutexLocker mLock(&m_sentDataMutex);
+
+ m_sentData.context.clear();
+ m_sentData.identity.clear();
+ } else {
+ // Check if the identity and/or the context has changed and if it did, send that new info to the server
+ QMutexLocker mLock(&m_sentDataMutex);
+ QReadLocker rLock(&m_positionalData.m_lock);
+
+ if (m_sentData.context != m_positionalData.m_context || m_sentData.identity != m_positionalData.m_identity ) {
+ MumbleProto::UserState mpus;
+ mpus.set_session(Global::get().uiSession);
+
+ if (m_sentData.context != m_positionalData.m_context) {
+ m_sentData.context = m_positionalData.m_context;
+ mpus.set_plugin_context(m_sentData.context.toUtf8().constData(), m_sentData.context.size());
+ }
+ if (m_sentData.identity != m_positionalData.m_identity) {
+ m_sentData.identity = m_positionalData.m_identity;
+ mpus.set_plugin_identity(m_sentData.identity.toUtf8().constData());
+ }
+
+ if (Global::get().sh) {
+ // send the message if the serverHandler is available
+ Global::get().sh->sendMessage(mpus);
+ }
+ }
+ }
+ }
+}
+
+void PluginManager::on_updatesAvailable() {
+ if (Global::get().s.bPluginAutoUpdate) {
+ m_updater.update();
+ } else {
+ m_updater.promptAndUpdate();
+ }
+}
+
+void PluginManager::checkForAvailablePositionalDataPlugin() {
+ bool performSearch = false;
+ {
+ QReadLocker activePluginLock(&m_activePosDataPluginLock);
+
+ performSearch = !m_activePositionalDataPlugin;
+ }
+
+ if (performSearch) {
+ selectActivePositionalDataPlugin();
+ }
+}
diff --git a/src/mumble/PluginManager.h b/src/mumble/PluginManager.h
new file mode 100644
index 000000000..02f4db5fa
--- /dev/null
+++ b/src/mumble/PluginManager.h
@@ -0,0 +1,279 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGINMANAGER_H_
+#define MUMBLE_MUMBLE_PLUGINMANAGER_H_
+
+#include <QObject>
+#include <QReadWriteLock>
+#include <QString>
+#include <QTimer>
+#include <QHash>
+#include <QMutex>
+#ifdef Q_OS_WIN
+ #ifndef NOMINMAX
+ #define NOMINMAX
+ #endif
+ #include <windows.h>
+#endif
+#include "Plugin.h"
+#include "MumbleApplication.h"
+#include "PositionalData.h"
+
+#include "User.h"
+#include "ClientUser.h"
+#include "Channel.h"
+#include "Settings.h"
+#include "PluginUpdater.h"
+
+#include <functional>
+
+/// A struct for holding the values of the current context and identity that have been sent to the server
+struct PluginManager_SentData {
+ QString context;
+ QString identity;
+};
+
+
+/// The plugin manager is the central object dealing with everything plugin-related. It is responsible for
+/// finding, loading and managing the plugins. It also is responsible for invoking callback functions in the plugins
+/// and can be used by Mumble to communicate with them
+class PluginManager : public QObject {
+ private:
+ Q_OBJECT
+ Q_DISABLE_COPY(PluginManager)
+ protected:
+ /// Lock for pluginHashMap. This lock has to be aquired when accessing pluginHashMap
+ mutable QReadWriteLock m_pluginCollectionLock;
+ /// A map between plugin-IDs and the actual plugin objects. You have to aquire pluginCollectionLock before
+ /// accessing this map.
+ QHash<plugin_id_t, plugin_ptr_t> m_pluginHashMap;
+ /// A set of directories to search plugins in
+ QSet<QString> m_pluginSearchPaths;
+#ifdef Q_OS_WIN
+ // This stuff is apparently needed on Windows in order to deal with DLLs
+ HANDLE m_hToken;
+ TOKEN_PRIVILEGES m_tpPrevious;
+ DWORD m_cbPrevious;
+#endif
+ /// The PositionalData object holding the current positional data (as retrieved by the respective plugin)
+ PositionalData m_positionalData;
+
+ /// A timer that causes the manager to regularly check for available plugins that can currently
+ /// deliver positional data.
+ QTimer m_positionalDataCheckTimer;
+
+ /// The mutex for sentData. This has to be aquired before accessing sentData
+ mutable QMutex m_sentDataMutex;
+ /// The bits of the positional data that have already been sent to the server. It is used to determine whether
+ /// the new data has to be sent to the server (in case it has changed). You have ti aquire sentDataMutex before
+ /// accessing this field.
+ PluginManager_SentData m_sentData;
+
+ /// The lock for activePositionalDataPlugin. It has to be aquired before accessing the respective field.
+ mutable QReadWriteLock m_activePosDataPluginLock;
+ /// The plugin that is currently used to retrieve positional data. You have to aquire activePosDataPluginLock before
+ /// accessing this field.
+ plugin_ptr_t m_activePositionalDataPlugin;
+ /// The PluginUpdater used to handle plugin updates.
+ PluginUpdater m_updater;
+
+ // We override the QObject::eventFilter function in order to be able to install the pluginManager as an event filter
+ // to the main application in order to get notified about keystrokes.
+ bool eventFilter(QObject *target, QEvent *event) Q_DECL_OVERRIDE;
+
+ /// Unloads all plugins that are currently loaded.
+ void unloadPlugins() const;
+ /// Clears the current list of plugins
+ void clearPlugins();
+ /// Iterates over the plugins and tries to select a plugin that currently claims to be able to deliver positional data. If
+ /// it found a plugin, activePositionalDataPlugin is set accordingly. If not, it is set to nullptr.
+ ///
+ /// @returns Whether this function succeeded in finding such a plugin
+ bool selectActivePositionalDataPlugin();
+
+ /// A internal helper function that iterates over all plugins and calls the given function providing the current plugin as
+ /// a parameter.
+ void foreachPlugin(std::function<void(Plugin&)>) const;
+ public:
+ // How often positional data (identity & context) should be synched with the server if there is any (in ms)
+ static constexpr int POSITIONAL_SERVER_SYNC_INTERVAL = 500;
+ // How often the manager should check for available positional data plugins
+ static constexpr int POSITIONAL_DATA_CHECK_INTERVAL = 1000;
+
+ /// Constructor
+ ///
+ /// @param additionalSearchPaths A pointer to a set of additional search paths or nullptr if no additional
+ /// paths are required.
+ /// @param p The parent QObject
+ PluginManager(QSet<QString> *additionalSearchPaths = nullptr, QObject *p = nullptr);
+ /// Destructor
+ virtual ~PluginManager() Q_DECL_OVERRIDE;
+
+ /// @param pluginID The ID of the plugin that should be retreved
+ /// @returns A pointer to the plugin with the given ID or nullptr if no such plugin could be found
+ const_plugin_ptr_t getPlugin(plugin_id_t pluginID) const;
+ /// Checks whether there are any updates for the plugins and if there are it invokes the PluginUpdater.
+ void checkForPluginUpdates();
+ /// Fetches positional data from the activePositionalDataPlugin if there is one set. This function will update the
+ /// positionalData field
+ ///
+ /// @returns Whether the positional data could be retrieved successfully
+ bool fetchPositionalData();
+ /// Unlinks the currently active positional data plugin. Effectively this sets activePositionalDataPlugin to nullptr
+ void unlinkPositionalData();
+ /// @returns Whether positional data is currently available (it has been successfully set via fetchPositionalData)
+ bool isPositionalDataAvailable() const;
+ /// @returns The most recent positional data
+ const PositionalData& getPositionalData() const;
+ /// Enables positional data gathering for the plugin with the given ID. A plugin is only even asked whether it can deliver
+ /// positional data if this is enabled.
+ ///
+ /// @param pluginID The ID of the plugin to access
+ /// @param enable Whether to enable positional data (alternative is to disable it)
+ void enablePositionalDataFor(plugin_id_t pluginID, bool enable = true) const;
+ /// @returns A const vector of the plugins
+ const QVector<const_plugin_ptr_t> getPlugins(bool sorted = false) const;
+ /// Loads the plugin with the given ID. Loading means initializing the plugin.
+ ///
+ /// @param pluginID The ID of the plugin to load
+ /// @returns Whether the plugin could be successfully loaded
+ bool loadPlugin(plugin_id_t pluginID) const;
+ /// Unloads the plugin with the given ID. Unloading means shutting the plugign down.
+ ///
+ /// @param pluginID The ID of the plugin to unload
+ void unloadPlugin(plugin_id_t pluginID) const;
+ /// Unloads the given plugin. Unloading means shutting the plugign down.
+ ///
+ /// @param plugin The plugin to unload
+ void unloadPlugin(Plugin &plugin) const;
+ /// Clears the plugin from the list of known plugins
+ ///
+ /// @param pluginID The ID of the plugin to forget about
+ /// @returns Whether the plugin has been cleared successfully
+ bool clearPlugin(plugin_id_t pluginID);
+ /// Deactivates the given features for the plugin with the given ID
+ ///
+ /// @param pluginID The ID of the plugin to access
+ /// @param features The feature set that should be deactivated. The features are or'ed together.
+ /// @returns The feature set that could not be deactivated
+ uint32_t deactivateFeaturesFor(plugin_id_t pluginID, uint32_t features) const;
+ /// Allows or forbids the given plugin to monitor keyboard events.
+ ///
+ /// @param pluginID The ID of the plugin to access
+ /// @param allow Whether to allow the monitoring or not
+ void allowKeyboardMonitoringFor(plugin_id_t pluginID, bool allow) const;
+ /// Checks whether a plugin with the given ID exists.
+ ///
+ /// @param pluginID The ID to check
+ /// @returns Whether such a plugin exists
+ bool pluginExists(plugin_id_t pluginID) const;
+
+ public slots:
+ /// Rescans the plugin directory and load all plugins from there after having cleared the current plugin list
+ void rescanPlugins();
+ /// Slot that gets called whenever data from another plugin has been received. This function will then delegate
+ /// this to the respective plugin callback
+ ///
+ /// @param sender A pointer to the ClientUser whose client has sent the data
+ /// @param data The byte-array representing the sent data
+ /// @param dataLength The length of the data array
+ /// @param dataID The ID of the data
+ void on_receiveData(const ClientUser *sender, const uint8_t *data, size_t dataLength, const char *dataID) const;
+ /// Slot that gets called when the local client connects to a server. It will delegate it to the respective plugin callback.
+ void on_serverConnected() const;
+ /// Slot that gets called when the local client disconnects to a server. It will delegate it to the respective plugin callback.
+ void on_serverDisconnected() const;
+ /// Slot that gets called when a client enters a channel. It will delegate it to the respective plugin callback.
+ ///
+ /// @param newChannel A pointer to the new channel
+ /// @param prevChannel A pointer to the previous channel or nullptr if no such channel exists
+ /// @param user A pointer to the user that entered the channel
+ void on_channelEntered(const Channel *newChannel, const Channel *prevChannel, const User *user) const;
+ /// Slot that gets called when a client leaves a channel. It will delegate it to the respective plugin callback.
+ ///
+ /// @param channel A pointer to the channel that has been left
+ /// @param user A pointer to the user that entered the channel
+ void on_channelExited(const Channel *channel, const User *user) const;
+ /// Slot that gets called when the local client changes its talking state. It will delegate it to the respective plugin callback.
+ void on_userTalkingStateChanged() const;
+ /// Slot that gets called when the local client receives audio input. It will delegate it to the respective plugin callback.
+ ///
+ /// @param inputPCM The array containing the input PCM (pulse-code-modulation). Its length is sampleCount * channelCount
+ /// @param sampleCount The amount of samples in the PCM array
+ /// @param channelCount The amount of channels in the PCM array
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers this input as speech
+ void on_audioInput(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech) const;
+ /// Slot that gets called when the local client has fetched an audio source. It will delegate it to the respective plugin callback.
+ ///
+ /// @param outputPCM The array containing the output-PCM (pulse-code-modulation). Its length is sampleCount * channelCount
+ /// @param sampleCount The amount of samples in the PCM array
+ /// @param channelCount The amount of channels in the PCM array
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers this input as speech
+ /// @param user A pointer to the ClientUser the audio source corresposnds to
+ void on_audioSourceFetched(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech,
+ const ClientUser *user) const;
+ /// Slot that gets called when the local client is about to play some audio. It will delegate it to the respective plugin callback.
+ ///
+ /// @param outputPCM The array containing the output-PCM (pulse-code-modulation). Its length is sampleCount * channelCount
+ /// @param sampleCount The amount of samples in the PCM array
+ /// @param channelCount The amount of channels in the PCM array
+ /// @param sampleRate The used sample rate in Hz
+ void on_audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate,
+ bool *modifiedAudio) const;
+ /// Slot that gets called after the local client has finished synchronizing with the server. It will delegate it to the respective
+ /// plugin callback.
+ void on_serverSynchronized() const;
+ /// Slot that gets called when a new user is added to the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param userID The ID of the added user
+ void on_userAdded(unsigned int userID) const;
+ /// Slot that gets called when a user is removed from the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param userID The ID of the removed user
+ void on_userRemoved(unsigned int userID) const;
+ /// Slot that gets called when a new channel is added to the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param channelID The ID of the added channel
+ void on_channelAdded(int channelID) const;
+ /// Slot that gets called when a channel is removed from the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param channelID The ID of the removed channel
+ void on_channelRemoved(int channelID) const;
+ /// Slot that gets called when a channel is renamed. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param channelID The ID of the renamed channel
+ void on_channelRenamed(int channelID) const;
+ /// Slot that gets called when a key has been pressed or released while Mumble has keyboard focus.
+ ///
+ /// @param key The code of the affected key (as encoded by Qt::Key)
+ /// @param modifiers The modifiers that were active in the moment of the event
+ /// @param isPress True if the key has been pressed, false if it has been released
+ void on_keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress) const;
+
+ /// Slot that gets called whenever the positional data should be synchronized with the server. Before it does that, it tries to
+ /// fetch new data.
+ void on_syncPositionalData();
+ /// Slot called if there are plugin updates available
+ void on_updatesAvailable();
+
+ protected slots:
+ /// If there is no active positional data plugin, this function will initiate searching for a
+ /// new one.
+ void checkForAvailablePositionalDataPlugin();
+
+ signals:
+ /// A signal emitted if the PluginManager (acting as an event filter) detected
+ /// a QKeyEvent.
+ ///
+ /// @param key The code of the affected key (as encoded by Qt::Key)
+ /// @param modifiers The modifiers that were active in the moment of the event
+ /// @param isPress True if the key has been pressed, false if it has been released
+ void keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress);
+};
+
+#endif
diff --git a/src/mumble/PluginUpdater.cpp b/src/mumble/PluginUpdater.cpp
new file mode 100644
index 000000000..e7f4f2594
--- /dev/null
+++ b/src/mumble/PluginUpdater.cpp
@@ -0,0 +1,379 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "PluginUpdater.h"
+#include "PluginManager.h"
+#include "Log.h"
+#include "PluginInstaller.h"
+#include "Global.h"
+
+#include <QtWidgets/QCheckBox>
+#include <QtWidgets/QLabel>
+#include <QtCore/QHashIterator>
+#include <QtCore/QSignalBlocker>
+#include <QtCore/QByteArray>
+#include <QtCore/QDir>
+#include <QtCore/QFile>
+#include <QtConcurrent>
+#include <QNetworkRequest>
+
+#include <algorithm>
+
+PluginUpdater::PluginUpdater(QWidget *parent)
+ : QDialog(parent),
+ m_wasInterrupted(false),
+ m_dataMutex(),
+ m_pluginsToUpdate(),
+ m_networkManager(),
+ m_pluginUpdateWidgets() {
+
+ QObject::connect(&m_networkManager, &QNetworkAccessManager::finished, this, &PluginUpdater::on_updateDownloaded);
+}
+
+PluginUpdater::~PluginUpdater() {
+ m_wasInterrupted.store(true);
+}
+
+void PluginUpdater::checkForUpdates() {
+ // Dispatch a thread in which each plugin can check for updates
+ QtConcurrent::run([this]() {
+ QMutexLocker lock(&m_dataMutex);
+
+ const QVector<const_plugin_ptr_t> plugins = Global::get().pluginManager->getPlugins();
+
+ for (int i = 0; i < plugins.size(); i++) {
+ const_plugin_ptr_t plugin = plugins[i];
+
+ if (plugin->hasUpdate()) {
+ QUrl updateURL = plugin->getUpdateDownloadURL();
+
+ if (updateURL.isValid() && !updateURL.isEmpty() && !updateURL.fileName().isEmpty()) {
+ UpdateEntry entry = { plugin->getID(), updateURL, updateURL.fileName(), 0 };
+ m_pluginsToUpdate << entry;
+ }
+ }
+
+ // if the update has been asked to be interrupted, exit here
+ if (m_wasInterrupted.load()) {
+ emit updateInterrupted();
+ return;
+ }
+ }
+
+ if (!m_pluginsToUpdate.isEmpty()) {
+ emit updatesAvailable();
+ }
+ });
+}
+
+void PluginUpdater::promptAndUpdate() {
+ setupUi(this);
+ populateUI();
+
+ setWindowIcon(QIcon(QLatin1String("skin:mumble.svg")));
+
+ QObject::connect(qcbSelectAll, &QCheckBox::stateChanged, this, &PluginUpdater::on_selectAll);
+ QObject::connect(this, &QDialog::finished, this, &PluginUpdater::on_finished);
+
+ if (exec() == QDialog::Accepted) {
+ update();
+ }
+}
+
+void PluginUpdater::update() {
+ QMutexLocker l(&m_dataMutex);
+
+ for (int i = 0; i < m_pluginsToUpdate.size(); i++) {
+ UpdateEntry currentEntry = m_pluginsToUpdate[i];
+
+ // The network manager will be emit a signal once the request has finished processing.
+ // Thus we can ignore the returned QNetworkReply* here.
+ m_networkManager.get(QNetworkRequest(currentEntry.updateURL));
+ }
+}
+
+void PluginUpdater::populateUI() {
+ clearUI();
+
+ QMutexLocker l(&m_dataMutex);
+ for (int i = 0; i < m_pluginsToUpdate.size(); i++) {
+ UpdateEntry currentEntry = m_pluginsToUpdate[i];
+ plugin_id_t pluginID = currentEntry.pluginID;
+
+ const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(pluginID);
+
+ if (!plugin) {
+ continue;
+ }
+
+ QCheckBox *checkBox = new QCheckBox(qwContent);
+ checkBox->setText(plugin->getName());
+ checkBox->setToolTip(plugin->getDescription());
+
+ checkBox->setProperty("pluginID", pluginID);
+
+ QLabel *urlLabel = new QLabel(qwContent);
+ urlLabel->setText(currentEntry.updateURL.toString());
+ urlLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
+
+ UpdateWidgetPair pair = { checkBox, urlLabel };
+ m_pluginUpdateWidgets << pair;
+
+ QObject::connect(checkBox, &QCheckBox::stateChanged, this, &PluginUpdater::on_singleSelectionChanged);
+ }
+
+ // sort the plugins alphabetically
+ std::sort(m_pluginUpdateWidgets.begin(), m_pluginUpdateWidgets.end(), [](const UpdateWidgetPair &first, const UpdateWidgetPair &second) {
+ return first.pluginCheckBox->text().compare(second.pluginCheckBox->text(), Qt::CaseInsensitive) < 0;
+ });
+
+ // add the widgets to the layout
+ for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
+ UpdateWidgetPair &currentPair = m_pluginUpdateWidgets[i];
+
+ static_cast<QFormLayout*>(qwContent->layout())->addRow(currentPair.pluginCheckBox, currentPair.urlLabel);
+ }
+}
+
+void PluginUpdater::clearUI() {
+ // There are always as many checkboxes as there are labels
+ for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
+ UpdateWidgetPair &currentPair = m_pluginUpdateWidgets[i];
+
+ qwContent->layout()->removeWidget(currentPair.pluginCheckBox);
+ qwContent->layout()->removeWidget(currentPair.urlLabel);
+
+ delete currentPair.pluginCheckBox;
+ delete currentPair.urlLabel;
+ }
+}
+
+void PluginUpdater::on_selectAll(int checkState) {
+ // failsafe for partially selected state (shouldn't happen though)
+ if (checkState == Qt::PartiallyChecked) {
+ checkState = Qt::Unchecked;
+ }
+
+ // Select or deselect all plugins
+ for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
+ UpdateWidgetPair &currentPair = m_pluginUpdateWidgets[i];
+
+ currentPair.pluginCheckBox->setCheckState(static_cast<Qt::CheckState>(checkState));
+ }
+}
+
+void PluginUpdater::on_singleSelectionChanged(int checkState) {
+ bool isChecked = checkState == Qt::Checked;
+
+ // Block signals for the selectAll checkBox in order to not trigger its
+ // check-logic when changing its check-state here
+ const QSignalBlocker blocker(qcbSelectAll);
+
+ if (!isChecked) {
+ // If even a single item is unchecked, the selectAll checkbox has to be unchecked
+ qcbSelectAll->setCheckState(Qt::Unchecked);
+ return;
+ }
+
+ // iterate through all checkboxes to see whether we have to toggle the selectAll checkbox
+ for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
+ const UpdateWidgetPair &currentPair = m_pluginUpdateWidgets[i];
+
+ if (!currentPair.pluginCheckBox->isChecked()) {
+ // One unchecked checkBox is enough to know that the selectAll
+ // CheckBox can't be checked, so we can abort at this point
+ return;
+ }
+ }
+
+ qcbSelectAll->setCheckState(Qt::Checked);
+}
+
+void PluginUpdater::on_finished(int result) {
+ if (result == QDialog::Accepted) {
+ if (qcbSelectAll->isChecked()) {
+ // all plugins shall be updated, so we don't have to check them individually
+ return;
+ }
+
+ QMutexLocker l(&m_dataMutex);
+
+ // The user wants to update the selected plugins only
+ // remove the plugins that shouldn't be updated from m_pluginsToUpdate
+ auto it = m_pluginsToUpdate.begin();
+ while (it != m_pluginsToUpdate.end()) {
+ plugin_id_t id = it->pluginID;
+
+ // find the corresponding checkbox
+ bool updateCurrent = false;
+ for (int k = 0; k < m_pluginUpdateWidgets.size(); k++) {
+ QCheckBox *checkBox = m_pluginUpdateWidgets[k].pluginCheckBox;
+ QVariant idVariant = checkBox->property("pluginID");
+
+ if (idVariant.isValid() && static_cast<plugin_id_t>(idVariant.toInt()) == id) {
+ updateCurrent = checkBox->isChecked();
+ break;
+ }
+ }
+
+ if (!updateCurrent) {
+ // remove this entry from the update-vector
+ it = m_pluginsToUpdate.erase(it);
+ } else {
+ it++;
+ }
+ }
+ } else {
+ // Nothing to do as the user doesn't want to update anyways
+ }
+}
+
+void PluginUpdater::interrupt() {
+ m_wasInterrupted.store(true);
+}
+
+void PluginUpdater::on_updateDownloaded(QNetworkReply *reply) {
+ if (reply) {
+ // Schedule reply for deletion
+ reply->deleteLater();
+
+ if (m_wasInterrupted.load()) {
+ emit updateInterrupted();
+ return;
+ }
+
+ // Find the ID of the plugin this update is for by comparing the URLs
+ UpdateEntry entry;
+ bool foundID = false;
+ {
+ QMutexLocker l(&m_dataMutex);
+
+ for (int i = 0; i < m_pluginsToUpdate.size(); i++) {
+ if (m_pluginsToUpdate[i].updateURL == reply->url()) {
+ foundID = true;
+
+ // remove that entry from the vector as it is being updated right here
+ entry = m_pluginsToUpdate.takeAt(i);
+ break;
+ }
+ }
+ }
+
+ if (!foundID) {
+ // Can't match the URL to a pluginID
+ qWarning() << "PluginUpdater: Requested update for plugin from"
+ << reply->url() << "but didn't find corresponding plugin again!";
+ return;
+ }
+
+ // Now get a handle to that plugin
+ const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(entry.pluginID);
+
+ if (!plugin) {
+ // Can't find plugin with given ID
+ qWarning() << "PluginUpdater: Got update for plugin with id"
+ << entry.pluginID << "but it doesn't seem to exist anymore!";
+ return;
+ }
+
+ // We can start actually checking the reply here
+ if (reply->error() != QNetworkReply::NoError) {
+ // There was an error during this request. Report it
+ Log::logOrDefer(Log::Warning,
+ tr("Unable to download plugin update for \"%1\" from \"%2\" (%3)").arg(
+ plugin->getName()).arg(reply->url().toString()).arg(
+ QString::fromLatin1(
+ QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(reply->error())
+ )
+ )
+ );
+ return;
+ }
+
+ // Check HTTP status code (just because the request was successful, doesn't
+ // mean the data was downloaded successfully
+ int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (httpStatusCode >= 300 && httpStatusCode < 400) {
+ // We have been redirected
+ if (entry.redirects >= MAX_REDIRECTS - 1) {
+ // Maximum redirect count exceeded
+ Log::logOrDefer(Log::Warning, tr("Update for plugin \"%1\" failed due to too many redirects").arg(plugin->getName()));
+
+ return;
+ }
+
+ QUrl redirectedUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
+ // Because the redirection url can be relative,
+ // we have to use the previous one to resolve it
+ redirectedUrl = reply->url().resolved(redirectedUrl);
+
+ // Re-insert the current plugin into the list of updating plugins (using the
+ // new URL so that it will be associated with that instead of the old one)
+ entry.updateURL = redirectedUrl;
+ entry.redirects++;
+ {
+ QMutexLocker l(&m_dataMutex);
+
+ m_pluginsToUpdate << entry;
+ }
+
+ // Post a new request for the file to the new URL
+ m_networkManager.get(QNetworkRequest(redirectedUrl));
+
+ return;
+ }
+
+ if (httpStatusCode < 200 || httpStatusCode >= 300) {
+ // HTTP request has failed
+ Log::logOrDefer(Log::Warning,
+ tr("Unable to download plugin update for \"%1\" from \"%2\" (HTTP status code %3)").arg(
+ plugin->getName()).arg(reply->url().toString()).arg(httpStatusCode)
+ );
+
+ return;
+ }
+
+ // Reply seems fine -> write file to disk and fire installer
+ QByteArray content = reply->readAll();
+
+ // Write the content to a file in the temp-dir
+ if (content.isEmpty()) {
+ qWarning() << "PluginUpdater: Update for" << plugin->getName() << "from"
+ << reply->url().toString() << "resulted in no content!";
+ return;
+ }
+
+ QFile file(QDir::temp().filePath(entry.fileName));
+ if (!file.open(QIODevice::WriteOnly)) {
+ qWarning() << "PluginUpdater: Can't open" << file.fileName() << "for writing!";
+ return;
+ }
+
+ file.write(content);
+ file.close();
+
+ try {
+ // Launch installer
+ PluginInstaller installer(QFileInfo(file.fileName()));
+ installer.install();
+
+ Log::logOrDefer(Log::Information, tr("Successfully updated plugin \"%1\"").arg(plugin->getName()));
+
+ // Make sure Mumble won't use the old version of the plugin
+ Global::get().pluginManager->rescanPlugins();
+ } catch (const PluginInstallException &e) {
+ Log::logOrDefer(Log::CriticalError, e.getMessage());
+ }
+
+ {
+ QMutexLocker l(&m_dataMutex);
+
+ if (m_pluginsToUpdate.isEmpty()) {
+ emit updatingFinished();
+ }
+ }
+ }
+}
diff --git a/src/mumble/PluginUpdater.h b/src/mumble/PluginUpdater.h
new file mode 100644
index 000000000..2d9041d3d
--- /dev/null
+++ b/src/mumble/PluginUpdater.h
@@ -0,0 +1,107 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGINUPDATER_H_
+#define MUMBLE_MUMBLE_PLUGINUPDATER_H_
+
+#include <QtCore/QVector>
+#include <QtCore/QUrl>
+#include <QtCore/QMutex>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+
+#include <atomic>
+
+#include "ui_PluginUpdater.h"
+#include "Plugin.h"
+
+/// A helper struct to store a pair of a CheckBox and a label corresponding to
+/// a single plugin.
+struct UpdateWidgetPair {
+ QCheckBox *pluginCheckBox;
+ QLabel *urlLabel;
+};
+
+/// A helper struct to store a pair of a plugin ID and an URL corresponding to
+/// the same plugin.
+struct UpdateEntry {
+ plugin_id_t pluginID;
+ QUrl updateURL;
+ QString fileName;
+ int redirects;
+};
+
+/// A class designed for managing plugin updates. At the same time this also represents
+/// a Dialog that can be used to prompt the user whether certain updates should be updated.
+class PluginUpdater : public QDialog, public Ui::PluginUpdater {
+ private:
+ Q_OBJECT;
+ Q_DISABLE_COPY(PluginUpdater);
+
+ protected:
+ /// An atomic flag indicating whether the plugin update has been interrupted. It is used
+ /// to exit some loops in different threads before they are done.
+ std::atomic<bool> m_wasInterrupted;
+ /// A mutex for m_pluginsToUpdate.
+ QMutex m_dataMutex;
+ /// A vector holding plugins that can be updated by storing a pluginID and the download URL
+ /// in form of an UpdateEntry.
+ QVector<UpdateEntry> m_pluginsToUpdate;
+ /// The NetworkManager used to perform the downloding of plugins.
+ QNetworkAccessManager m_networkManager;
+ /// A vector of the UI elements created for the individual plugins (in form of UpdateWidgetPairs).
+ /// NOTE: This vector may only be accessed from the UI thread this dialog is living in!
+ QVector<UpdateWidgetPair> m_pluginUpdateWidgets;
+
+ /// Populates the UI with plugins that have been found to have an update available (through a call
+ /// to checkForUpdates()).
+ void populateUI();
+
+ public:
+ /// Constructor
+ ///
+ /// @param parent A pointer to the QWidget parent of this object
+ PluginUpdater(QWidget *parent = nullptr);
+ /// Destructor
+ ~PluginUpdater();
+
+ // The maximum number of redirects to allow
+ static constexpr int MAX_REDIRECTS = 10;
+
+ /// Triggers an update check for all plugins that are currently recognized by Mumble. This is done
+ /// in a non-blocking fashion (in another thread). Once all plugins have been checked and if there
+ /// are updates available, the updatesAvailable signal is emitted.
+ void checkForUpdates();
+ /// Launches a Dialog that asks the user which of the plugins an update has been found for, shall be
+ /// updated. If the user has selected at least selected one plugin and has accepted the dialog, this
+ /// function will automatically call update().
+ void promptAndUpdate();
+ /// Starts the update process of the plugins. This is done asynchronously.
+ void update();
+ public slots:
+ /// Clears the UI from the widgets created for the individual plugins.
+ void clearUI();
+ /// Slot triggered if the user changes the state of the selectAll CheckBox.
+ void on_selectAll(int checkState);
+ /// Slot triggered if the user toggles the CheckBox for any individual plugin.
+ void on_singleSelectionChanged(int checkState);
+ /// Slot triggered when the dialog is being closed.
+ void on_finished(int result);
+ /// Slot that can be triggered to ask for the update process to be interrupted.
+ void interrupt();
+ protected slots:
+ /// Slot triggered once an update for a plugin has been downloaded.
+ void on_updateDownloaded(QNetworkReply *reply);
+
+ signals:
+ /// This signal is emitted once it has been determined that there are plugin updates available.
+ void updatesAvailable();
+ /// This signal is emitted once all plugin updates have been downloaded and processed.
+ void updatingFinished();
+ /// This signal is emitted every time the update process has been interrupted.
+ void updateInterrupted();
+};
+
+#endif // MUMBLE_MUMBLE_PLUGINUPDATER_H_
diff --git a/src/mumble/PluginUpdater.ui b/src/mumble/PluginUpdater.ui
new file mode 100644
index 000000000..8f2118d65
--- /dev/null
+++ b/src/mumble/PluginUpdater.ui
@@ -0,0 +1,224 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PluginUpdater</class>
+ <widget class="QDialog" name="PluginUpdater">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>616</width>
+ <height>460</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>PluginUpdater</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="modal">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="qlTitleText">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>The following plugins can be updated.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>7</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="Line" name="line_2">
+ <property name="styleSheet">
+ <string notr="true">background-color: rgb(94, 94, 94);</string>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="midLineWidth">
+ <number>0</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="qcbSelectAll">
+ <property name="text">
+ <string>Select all</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="qwContent">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>600</width>
+ <height>284</height>
+ </rect>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="labelAlignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <property name="horizontalSpacing">
+ <number>15</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="qlPlugin">
+ <property name="styleSheet">
+ <string notr="true">font-weight: bold;</string>
+ </property>
+ <property name="text">
+ <string>Plugin</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="qlURL">
+ <property name="styleSheet">
+ <string notr="true">font-weight: bold;</string>
+ </property>
+ <property name="text">
+ <string>Download-URL</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="styleSheet">
+ <string notr="true">background-color: rgb(94, 94, 94);</string>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="midLineWidth">
+ <number>0</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>7</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="qlUpdateSelected">
+ <property name="text">
+ <string>Do you want to update the selected plugins?</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::No|QDialogButtonBox::Yes</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>PluginUpdater</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>PluginUpdater</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/src/mumble/Plugins.cpp b/src/mumble/Plugins.cpp
deleted file mode 100644
index 4510f757c..000000000
--- a/src/mumble/Plugins.cpp
+++ /dev/null
@@ -1,792 +0,0 @@
-// Copyright 2007-2021 The Mumble Developers. All rights reserved.
-// Use of this source code is governed by a BSD-style license
-// that can be found in the LICENSE file at the root of the
-// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-
-#include "Plugins.h"
-
-#include "../../plugins/mumble_plugin.h"
-#include "Log.h"
-#include "MainWindow.h"
-#include "Message.h"
-#include "MumbleApplication.h"
-#include "ServerHandler.h"
-#include "Utils.h"
-#include "WebFetch.h"
-#ifdef USE_MANUAL_PLUGIN
-# include "ManualPlugin.h"
-#endif
-#include "Global.h"
-
-#include <QtCore/QLibrary>
-#include <QtCore/QUrlQuery>
-
-#ifdef Q_OS_WIN
-# include <QtCore/QTemporaryFile>
-#endif
-
-#include <QtWidgets/QMessageBox>
-#include <QtXml/QDomDocument>
-
-#ifdef Q_OS_WIN
-# include <softpub.h>
-# include <tlhelp32.h>
-#endif
-
-const QString PluginConfig::name = QLatin1String("PluginConfig");
-
-static ConfigWidget *PluginConfigDialogNew(Settings &st) {
- return new PluginConfig(st);
-}
-
-static ConfigRegistrar registrarPlugins(5000, PluginConfigDialogNew);
-
-struct PluginInfo {
- bool locked;
- bool enabled;
- QLibrary lib;
- QString filename;
- QString description;
- QString shortname;
- MumblePlugin *p;
- MumblePlugin2 *p2;
- MumblePluginQt *pqt;
- PluginInfo();
-};
-
-PluginInfo::PluginInfo() {
- locked = false;
- enabled = false;
- p = nullptr;
- p2 = nullptr;
- pqt = nullptr;
-}
-
-struct PluginFetchMeta {
- QString hash;
- QString path;
-
- PluginFetchMeta(const QString &hash_ = QString(), const QString &path_ = QString())
- : hash(hash_), path(path_) { /* Empty */
- }
-};
-
-
-PluginConfig::PluginConfig(Settings &st) : ConfigWidget(st) {
- setupUi(this);
- qtwPlugins->setAccessibleName(tr("Plugins"));
- qtwPlugins->header()->setSectionResizeMode(0, QHeaderView::Stretch);
- qtwPlugins->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
-
- refillPluginList();
-}
-
-QString PluginConfig::title() const {
- return tr("Plugins");
-}
-
-const QString &PluginConfig::getName() const {
- return PluginConfig::name;
-}
-
-QIcon PluginConfig::icon() const {
- return QIcon(QLatin1String("skin:config_plugin.png"));
-}
-
-void PluginConfig::load(const Settings &r) {
- loadCheckBox(qcbTransmit, r.bTransmitPosition);
-}
-
-void PluginConfig::save() const {
- QReadLocker lock(&Global::get().p->qrwlPlugins);
-
- s.bTransmitPosition = qcbTransmit->isChecked();
- s.qmPositionalAudioPlugins.clear();
-
- QList< QTreeWidgetItem * > list = qtwPlugins->findItems(QString(), Qt::MatchContains);
- foreach (QTreeWidgetItem *i, list) {
- bool enabled = (i->checkState(1) == Qt::Checked);
-
- PluginInfo *pi = pluginForItem(i);
- if (pi) {
- s.qmPositionalAudioPlugins.insert(pi->filename, enabled);
- pi->enabled = enabled;
- }
- }
-}
-
-PluginInfo *PluginConfig::pluginForItem(QTreeWidgetItem *i) const {
- if (i) {
- foreach (PluginInfo *pi, Global::get().p->qlPlugins) {
- if (pi->filename == i->data(0, Qt::UserRole).toString())
- return pi;
- }
- }
- return nullptr;
-}
-
-void PluginConfig::on_qpbConfig_clicked() {
- PluginInfo *pi;
- {
- QReadLocker lock(&Global::get().p->qrwlPlugins);
- pi = pluginForItem(qtwPlugins->currentItem());
- }
-
- if (!pi)
- return;
-
- if (pi->pqt && pi->pqt->config) {
- pi->pqt->config(this);
- } else if (pi->p->config) {
- pi->p->config(0);
- } else {
- QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no configure function."),
- QMessageBox::Ok, QMessageBox::NoButton);
- }
-}
-
-void PluginConfig::on_qpbAbout_clicked() {
- PluginInfo *pi;
- {
- QReadLocker lock(&Global::get().p->qrwlPlugins);
- pi = pluginForItem(qtwPlugins->currentItem());
- }
-
- if (!pi)
- return;
-
- if (pi->pqt && pi->pqt->about) {
- pi->pqt->about(this);
- } else if (pi->p->about) {
- pi->p->about(0);
- } else {
- QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no about function."), QMessageBox::Ok,
- QMessageBox::NoButton);
- }
-}
-
-void PluginConfig::on_qpbReload_clicked() {
- Global::get().p->rescanPlugins();
- refillPluginList();
-}
-
-void PluginConfig::refillPluginList() {
- QReadLocker lock(&Global::get().p->qrwlPlugins);
- qtwPlugins->clear();
-
- foreach (PluginInfo *pi, Global::get().p->qlPlugins) {
- QTreeWidgetItem *i = new QTreeWidgetItem(qtwPlugins);
- i->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
- i->setCheckState(1, pi->enabled ? Qt::Checked : Qt::Unchecked);
- i->setText(0, pi->description);
- if (pi->p->longdesc)
- i->setToolTip(0, QString::fromStdWString(pi->p->longdesc()).toHtmlEscaped());
- i->setData(0, Qt::UserRole, pi->filename);
- }
- qtwPlugins->setCurrentItem(qtwPlugins->topLevelItem(0));
- on_qtwPlugins_currentItemChanged(qtwPlugins->topLevelItem(0), nullptr);
-}
-
-void PluginConfig::on_qtwPlugins_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *) {
- QReadLocker lock(&Global::get().p->qrwlPlugins);
-
- PluginInfo *pi = pluginForItem(current);
- if (pi) {
- bool showAbout = false;
- if (pi->p->about) {
- showAbout = true;
- }
- if (pi->pqt && pi->pqt->about) {
- showAbout = true;
- }
- qpbAbout->setEnabled(showAbout);
-
- bool showConfig = false;
- if (pi->p->config) {
- showConfig = true;
- }
- if (pi->pqt && pi->pqt->config) {
- showConfig = true;
- }
- qpbConfig->setEnabled(showConfig);
- } else {
- qpbAbout->setEnabled(false);
- qpbConfig->setEnabled(false);
- }
-}
-
-Plugins::Plugins(QObject *p) : QObject(p) {
- QTimer *timer = new QTimer(this);
- timer->setObjectName(QLatin1String("Timer"));
- timer->start(500);
- locked = prevlocked = nullptr;
- bValid = false;
- iPluginTry = 0;
- for (int i = 0; i < 3; i++)
- fPosition[i] = fFront[i] = fTop[i] = 0.0;
- QMetaObject::connectSlotsByName(this);
-
-#ifdef QT_NO_DEBUG
-# ifndef MUMBLE_PLUGIN_PATH
- qsSystemPlugins =
- QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath());
-# ifdef Q_OS_MAC
- qsSystemPlugins = QString::fromLatin1("%1/../Plugins").arg(qApp->applicationDirPath());
-# endif
-# else
- qsSystemPlugins = QLatin1String(MUMTEXT(MUMBLE_PLUGIN_PATH));
-# endif
-
- qsUserPlugins = Global::get().qdBasePath.absolutePath() + QLatin1String("/Plugins");
-#else
-# ifdef MUMBLE_PLUGIN_PATH
- qsSystemPlugins = QLatin1String(MUMTEXT(MUMBLE_PLUGIN_PATH));
-# else
- qsSystemPlugins = QString();
-# endif
-
- qsUserPlugins = QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath());
-#endif
-
-#ifdef Q_OS_WIN
- // According to MS KB Q131065, we need this to OpenProcess()
-
- hToken = nullptr;
-
- if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken)) {
- if (GetLastError() == ERROR_NO_TOKEN) {
- ImpersonateSelf(SecurityImpersonation);
- OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken);
- }
- }
-
- TOKEN_PRIVILEGES tp;
- LUID luid;
- cbPrevious = sizeof(TOKEN_PRIVILEGES);
-
- LookupPrivilegeValue(nullptr, SE_DEBUG_NAME, &luid);
-
- tp.PrivilegeCount = 1;
- tp.Privileges[0].Luid = luid;
- tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
-
- AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &tpPrevious, &cbPrevious);
-#endif
-}
-
-Plugins::~Plugins() {
- clearPlugins();
-
-#ifdef Q_OS_WIN
- AdjustTokenPrivileges(hToken, FALSE, &tpPrevious, cbPrevious, nullptr, nullptr);
- CloseHandle(hToken);
-#endif
-}
-
-void Plugins::clearPlugins() {
- QWriteLocker lock(&Global::get().p->qrwlPlugins);
- foreach (PluginInfo *pi, qlPlugins) {
- if (pi->locked)
- pi->p->unlock();
- pi->lib.unload();
- delete pi;
- }
- qlPlugins.clear();
-}
-
-void Plugins::rescanPlugins() {
- clearPlugins();
-
- QWriteLocker lock(&Global::get().p->qrwlPlugins);
- prevlocked = locked = nullptr;
- bValid = false;
-
- QDir qd(qsSystemPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable);
- QDir qud(qsUserPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable);
- QFileInfoList libs = qud.entryInfoList() + qd.entryInfoList();
-
- QSet< QString > evaluated;
- foreach (const QFileInfo &libinfo, libs) {
- QString fname = libinfo.fileName();
- QString libname = libinfo.absoluteFilePath();
- if (!evaluated.contains(fname) && QLibrary::isLibrary(libname)) {
- PluginInfo *pi = new PluginInfo();
- pi->lib.setFileName(libname);
- pi->filename = fname;
- if (pi->lib.load()) {
- mumblePluginFunc mpf = reinterpret_cast< mumblePluginFunc >(pi->lib.resolve("getMumblePlugin"));
- if (mpf) {
- evaluated.insert(fname);
- pi->p = mpf();
-
- // Check whether the plugin has a valid plugin magic and that it's not retracted.
- // A retracted plugin is a plugin that clients should disregard, typically because
- // the game the plugin was written for now provides positional audio via the
- // link plugin (see null_plugin.cpp).
- if (pi->p && pi->p->magic == MUMBLE_PLUGIN_MAGIC && pi->p->shortname != L"Retracted") {
- pi->description = QString::fromStdWString(pi->p->description);
- pi->shortname = QString::fromStdWString(pi->p->shortname);
- pi->enabled = Global::get().s.qmPositionalAudioPlugins.value(pi->filename, true);
-
- mumblePlugin2Func mpf2 =
- reinterpret_cast< mumblePlugin2Func >(pi->lib.resolve("getMumblePlugin2"));
- if (mpf2) {
- pi->p2 = mpf2();
- if (pi->p2->magic != MUMBLE_PLUGIN_MAGIC_2) {
- pi->p2 = nullptr;
- }
- }
-
- mumblePluginQtFunc mpfqt =
- reinterpret_cast< mumblePluginQtFunc >(pi->lib.resolve("getMumblePluginQt"));
- if (mpfqt) {
- pi->pqt = mpfqt();
- if (pi->pqt->magic != MUMBLE_PLUGIN_MAGIC_QT) {
- pi->pqt = nullptr;
- }
- }
-
- qlPlugins << pi;
- continue;
- }
- }
- pi->lib.unload();
- } else {
- qWarning("Plugins: Failed to load %s: %s", qPrintable(pi->filename), qPrintable(pi->lib.errorString()));
- }
- delete pi;
- }
- }
-
- // Handle built-in plugins
- {
-#if defined(USE_MANUAL_PLUGIN)
- // Manual plugin
- PluginInfo *pi = new PluginInfo();
- pi->filename = QLatin1String("manual.builtin");
- pi->p = ManualPlugin_getMumblePlugin();
- pi->pqt = ManualPlugin_getMumblePluginQt();
- pi->description = QString::fromStdWString(pi->p->description);
- pi->shortname = QString::fromStdWString(pi->p->shortname);
- pi->enabled = Global::get().s.qmPositionalAudioPlugins.value(pi->filename, true);
- qlPlugins << pi;
-#endif
- }
-}
-
-bool Plugins::fetch() {
- if (Global::get().bPosTest) {
- fPosition[0] = fPosition[1] = fPosition[2] = 0.0f;
- fFront[0] = 0.0f;
- fFront[1] = 0.0f;
- fFront[2] = 1.0f;
- fTop[0] = 0.0f;
- fTop[1] = 1.0f;
- fTop[2] = 0.0f;
-
- for (int i = 0; i < 3; ++i) {
- fCameraPosition[i] = fPosition[i];
- fCameraFront[i] = fFront[i];
- fCameraTop[i] = fTop[i];
- }
-
- bValid = true;
- return true;
- }
-
- if (!locked) {
- bValid = false;
- return bValid;
- }
-
- QReadLocker lock(&qrwlPlugins);
- if (!locked) {
- bValid = false;
- return bValid;
- }
-
- if (!locked->enabled)
- bUnlink = true;
-
- bool ok;
- {
- QMutexLocker mlock(&qmPluginStrings);
- ok = locked->p->fetch(fPosition, fFront, fTop, fCameraPosition, fCameraFront, fCameraTop, ssContext,
- swsIdentity);
- }
- if (!ok || bUnlink) {
- lock.unlock();
- QWriteLocker wlock(&qrwlPlugins);
-
- if (locked) {
- locked->p->unlock();
- locked->locked = false;
- prevlocked = locked;
- locked = nullptr;
- for (int i = 0; i < 3; i++)
- fPosition[i] = fFront[i] = fTop[i] = fCameraPosition[i] = fCameraFront[i] = fCameraTop[i] = 0.0f;
- }
- }
- bValid = ok;
- return bValid;
-}
-
-void Plugins::on_Timer_timeout() {
- fetch();
-
- QReadLocker lock(&qrwlPlugins);
-
- if (prevlocked) {
- Global::get().l->log(Log::Information, tr("%1 lost link.").arg(prevlocked->shortname.toHtmlEscaped()));
- prevlocked = nullptr;
- }
-
-
- {
- QMutexLocker mlock(&qmPluginStrings);
-
- if (!locked) {
- ssContext.clear();
- swsIdentity.clear();
- }
-
- std::string context;
- if (locked)
- context.assign(u8(QString::fromStdWString(locked->p->shortname)) + static_cast< char >(0) + ssContext);
-
- if (!Global::get().uiSession) {
- ssContextSent.clear();
- swsIdentitySent.clear();
- } else if ((context != ssContextSent) || (swsIdentity != swsIdentitySent)) {
- MumbleProto::UserState mpus;
- mpus.set_session(Global::get().uiSession);
- if (context != ssContextSent) {
- ssContextSent.assign(context);
- mpus.set_plugin_context(context);
- }
- if (swsIdentity != swsIdentitySent) {
- swsIdentitySent.assign(swsIdentity);
- mpus.set_plugin_identity(u8(QString::fromStdWString(swsIdentitySent)));
- }
- if (Global::get().sh)
- Global::get().sh->sendMessage(mpus);
- }
- }
-
- if (locked) {
- return;
- }
-
- if (!Global::get().s.bTransmitPosition)
- return;
-
- lock.unlock();
- QWriteLocker wlock(&qrwlPlugins);
-
- if (qlPlugins.isEmpty())
- return;
-
- ++iPluginTry;
- if (iPluginTry >= qlPlugins.count())
- iPluginTry = 0;
-
- std::multimap< std::wstring, unsigned long long int > pids;
-#if defined(Q_OS_WIN)
- PROCESSENTRY32 pe;
-
- pe.dwSize = sizeof(pe);
- HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
- if (hSnap != INVALID_HANDLE_VALUE) {
- BOOL ok = Process32First(hSnap, &pe);
-
- while (ok) {
- pids.insert(
- std::pair< std::wstring, unsigned long long int >(std::wstring(pe.szExeFile), pe.th32ProcessID));
- ok = Process32Next(hSnap, &pe);
- }
- CloseHandle(hSnap);
- }
-#elif defined(Q_OS_LINUX)
- QDir d(QLatin1String("/proc"));
- QStringList entries = d.entryList();
- bool ok;
- foreach (const QString &entry, entries) {
- // Check if the entry is a PID
- // by checking whether it's a number.
- // If it is not, skip it.
- unsigned long long int pid = static_cast< unsigned long long int >(entry.toLongLong(&ok, 10));
- if (!ok) {
- continue;
- }
-
- QString exe = QFile::symLinkTarget(QString(QLatin1String("/proc/%1/exe")).arg(entry));
- QFileInfo fi(exe);
- QString firstPart = fi.baseName();
- QString completeSuffix = fi.completeSuffix();
- QString baseName;
- if (completeSuffix.isEmpty()) {
- baseName = firstPart;
- } else {
- baseName = firstPart + QLatin1String(".") + completeSuffix;
- }
-
- if (baseName == QLatin1String("wine-preloader") || baseName == QLatin1String("wine64-preloader")) {
- QFile f(QString(QLatin1String("/proc/%1/cmdline")).arg(entry));
- if (f.open(QIODevice::ReadOnly)) {
- QByteArray cmdline = f.readAll();
- f.close();
-
- int nul = cmdline.indexOf('\0');
- if (nul != -1) {
- cmdline.truncate(nul);
- }
-
- QString exe = QString::fromUtf8(cmdline);
- if (exe.contains(QLatin1String("\\"))) {
- int lastBackslash = exe.lastIndexOf(QLatin1String("\\"));
- if (exe.count() > lastBackslash + 1) {
- baseName = exe.mid(lastBackslash + 1);
- }
- }
- }
- }
-
- if (!baseName.isEmpty()) {
- pids.insert(std::pair< std::wstring, unsigned long long int >(baseName.toStdWString(), pid));
- }
- }
-#endif
-
- PluginInfo *pi = qlPlugins.at(iPluginTry);
- if (pi->enabled) {
- if (pi->p2 ? pi->p2->trylock(pids) : pi->p->trylock()) {
- pi->shortname = QString::fromStdWString(pi->p->shortname);
- Global::get().l->log(Log::Information, tr("%1 linked.").arg(pi->shortname.toHtmlEscaped()));
- pi->locked = true;
- bUnlink = false;
- locked = pi;
- }
- }
-}
-
-void Plugins::checkUpdates() {
- QUrl url;
- url.setPath(QLatin1String("/v1/pa-plugins"));
-
- QList< QPair< QString, QString > > queryItems;
- queryItems << qMakePair(QString::fromUtf8("ver"),
- QString::fromUtf8(QUrl::toPercentEncoding(QString::fromUtf8(MUMBLE_RELEASE))));
-#if defined(Q_OS_WIN)
-# if defined(Q_OS_WIN64)
- queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("WinX64"));
-# else
- queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("Win32"));
-# endif
- queryItems << qMakePair(QString::fromUtf8("abi"), QString::fromUtf8(MUMTEXT(_MSC_VER)));
-#elif defined(Q_OS_MAC)
-# if defined(USE_MAC_UNIVERSAL)
- queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("MacOSX-Universal"));
-# else
- queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("MacOSX"));
-# endif
-#else
- queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("Unix"));
-#endif
-
-
-#ifdef QT_NO_DEBUG
- QUrlQuery query;
- query.setQueryItems(queryItems);
- url.setQuery(query);
-
- WebFetch::fetch(QLatin1String("update"), url, this, SLOT(fetchedUpdatePAPlugins(QByteArray, QUrl)));
-#else
- Global::get().mw->msgBox(tr("Skipping plugin update in debug mode."));
-#endif
-}
-
-void Plugins::fetchedUpdatePAPlugins(QByteArray data, QUrl) {
- if (data.isNull())
- return;
-
- bool rescan = false;
- qmPluginFetchMeta.clear();
- QDomDocument doc;
- doc.setContent(data);
-
- QDomElement root = doc.documentElement();
- QDomNode n = root.firstChild();
- while (!n.isNull()) {
- QDomElement e = n.toElement();
- if (!e.isNull()) {
- if (e.tagName() == QLatin1String("plugin")) {
- QString name = QFileInfo(e.attribute(QLatin1String("name"))).fileName();
- QString hash = e.attribute(QLatin1String("hash"));
- QString path = e.attribute(QLatin1String("path"));
- qmPluginFetchMeta.insert(name, PluginFetchMeta(hash, path));
- }
- }
- n = n.nextSibling();
- }
-
- QDir qd(qsSystemPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable);
- QDir qdu(qsUserPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable);
-
- QFileInfoList libs = qd.entryInfoList();
- foreach (const QFileInfo &libinfo, libs) {
- QString libname = libinfo.absoluteFilePath();
- QString filename = libinfo.fileName();
- PluginFetchMeta pfm = qmPluginFetchMeta.value(filename);
- QString wanthash = pfm.hash;
- if (!wanthash.isNull() && QLibrary::isLibrary(libname)) {
- QFile f(libname);
- if (wanthash.isEmpty()) {
- // Outdated plugin
- if (f.exists()) {
- clearPlugins();
- f.remove();
- rescan = true;
- }
- } else if (f.open(QIODevice::ReadOnly)) {
- QString h = QLatin1String(sha1(f.readAll()).toHex());
- f.close();
- if (h == wanthash) {
- if (qd != qdu) {
- QFile qfuser(qsUserPlugins + QString::fromLatin1("/") + filename);
- if (qfuser.exists()) {
- clearPlugins();
- qfuser.remove();
- rescan = true;
- }
- }
- // Mark for removal from userplugins
- qmPluginFetchMeta.insert(filename, PluginFetchMeta());
- }
- }
- }
- }
-
- if (qd != qdu) {
- libs = qdu.entryInfoList();
- foreach (const QFileInfo &libinfo, libs) {
- QString libname = libinfo.absoluteFilePath();
- QString filename = libinfo.fileName();
- PluginFetchMeta pfm = qmPluginFetchMeta.value(filename);
- QString wanthash = pfm.hash;
- if (!wanthash.isNull() && QLibrary::isLibrary(libname)) {
- QFile f(libname);
- if (wanthash.isEmpty()) {
- // Outdated plugin
- if (f.exists()) {
- clearPlugins();
- f.remove();
- rescan = true;
- }
- } else if (f.open(QIODevice::ReadOnly)) {
- QString h = QLatin1String(sha1(f.readAll()).toHex());
- f.close();
- if (h == wanthash) {
- qmPluginFetchMeta.remove(filename);
- }
- }
- }
- }
- }
- QMap< QString, PluginFetchMeta >::const_iterator i;
- for (i = qmPluginFetchMeta.constBegin(); i != qmPluginFetchMeta.constEnd(); ++i) {
- PluginFetchMeta pfm = i.value();
- if (!pfm.hash.isEmpty()) {
- QUrl pluginDownloadUrl;
- if (pfm.path.isEmpty()) {
- pluginDownloadUrl.setPath(QString::fromLatin1("%1").arg(i.key()));
- } else {
- pluginDownloadUrl.setPath(pfm.path);
- }
-
- WebFetch::fetch(QLatin1String("pa-plugin-dl"), pluginDownloadUrl, this,
- SLOT(fetchedPAPluginDL(QByteArray, QUrl)));
- }
- }
-
- if (rescan)
- rescanPlugins();
-}
-
-void Plugins::fetchedPAPluginDL(QByteArray data, QUrl url) {
- if (data.isNull())
- return;
-
- bool rescan = false;
-
- const QString &urlPath = url.path();
- QString fname = QFileInfo(urlPath).fileName();
- if (qmPluginFetchMeta.contains(fname)) {
- PluginFetchMeta pfm = qmPluginFetchMeta.value(fname);
- if (pfm.hash == QLatin1String(sha1(data).toHex())) {
- bool verified = true;
-#ifdef Q_OS_WIN
- verified = false;
- QString tempname;
- std::wstring tempnative;
- {
- QTemporaryFile temp(QDir::tempPath() + QLatin1String("/plugin_XXXXXX.dll"));
- if (temp.open()) {
- tempname = temp.fileName();
- tempnative = QDir::toNativeSeparators(tempname).toStdWString();
- temp.write(data);
- temp.setAutoRemove(false);
- }
- }
- if (!tempname.isNull()) {
- WINTRUST_FILE_INFO file;
- ZeroMemory(&file, sizeof(file));
- file.cbStruct = sizeof(file);
- file.pcwszFilePath = tempnative.c_str();
-
- WINTRUST_DATA data;
- ZeroMemory(&data, sizeof(data));
- data.cbStruct = sizeof(data);
- data.dwUIChoice = WTD_UI_NONE;
- data.fdwRevocationChecks = WTD_REVOKE_NONE;
- data.dwUnionChoice = WTD_CHOICE_FILE;
- data.pFile = &file;
- data.dwProvFlags = WTD_SAFER_FLAG | WTD_USE_DEFAULT_OSVER_CHECK;
- data.dwUIContext = WTD_UICONTEXT_INSTALL;
-
- static GUID guid = WINTRUST_ACTION_GENERIC_VERIFY_V2;
-
- LONG ts = WinVerifyTrust(0, &guid, &data);
-
- QFile deltemp(tempname);
- deltemp.remove();
- verified = (ts == 0);
- }
-#endif
- if (verified) {
- clearPlugins();
-
- QFile f;
- f.setFileName(qsSystemPlugins + QLatin1String("/") + fname);
- if (f.open(QIODevice::WriteOnly)) {
- f.write(data);
- f.close();
- Global::get().mw->msgBox(tr("Downloaded new or updated plugin to %1.").arg(f.fileName().toHtmlEscaped()));
- } else {
- f.setFileName(qsUserPlugins + QLatin1String("/") + fname);
- if (f.open(QIODevice::WriteOnly)) {
- f.write(data);
- f.close();
- Global::get().mw->msgBox(tr("Downloaded new or updated plugin to %1.").arg(f.fileName().toHtmlEscaped()));
- } else {
- Global::get().mw->msgBox(tr("Failed to install new plugin to %1.").arg(f.fileName().toHtmlEscaped()));
- }
- }
-
- rescan = true;
- }
- }
- }
-
- if (rescan)
- rescanPlugins();
-}
diff --git a/src/mumble/Plugins.h b/src/mumble/Plugins.h
deleted file mode 100644
index fd951bb73..000000000
--- a/src/mumble/Plugins.h
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright 2007-2021 The Mumble Developers. All rights reserved.
-// Use of this source code is governed by a BSD-style license
-// that can be found in the LICENSE file at the root of the
-// Mumble source tree or at <https://www.mumble.info/LICENSE>.
-
-#ifndef MUMBLE_MUMBLE_PLUGINS_H_
-#define MUMBLE_MUMBLE_PLUGINS_H_
-
-#include "ConfigDialog.h"
-
-#include "ui_Plugins.h"
-
-#ifdef Q_OS_WIN
-# include "win.h"
-#endif
-
-#include <QtCore/QMutex>
-#include <QtCore/QObject>
-#include <QtCore/QReadWriteLock>
-#include <QtCore/QUrl>
-
-struct PluginInfo;
-
-class PluginConfig : public ConfigWidget, public Ui::PluginConfig {
-private:
- Q_OBJECT
- Q_DISABLE_COPY(PluginConfig)
-protected:
- void refillPluginList();
- PluginInfo *pluginForItem(QTreeWidgetItem *) const;
-
-public:
- /// The unique name of this ConfigWidget
- static const QString name;
- PluginConfig(Settings &st);
- virtual QString title() const Q_DECL_OVERRIDE;
- const QString &getName() const Q_DECL_OVERRIDE;
- virtual QIcon icon() const Q_DECL_OVERRIDE;
-public slots:
- void save() const Q_DECL_OVERRIDE;
- void load(const Settings &r) Q_DECL_OVERRIDE;
- void on_qpbConfig_clicked();
- void on_qpbAbout_clicked();
- void on_qpbReload_clicked();
- void on_qtwPlugins_currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *);
-};
-
-struct PluginFetchMeta;
-
-class Plugins : public QObject {
- friend class PluginConfig;
-
-private:
- Q_OBJECT
- Q_DISABLE_COPY(Plugins)
-protected:
- QReadWriteLock qrwlPlugins;
- QMutex qmPluginStrings;
- QList< PluginInfo * > qlPlugins;
- PluginInfo *locked;
- PluginInfo *prevlocked;
- void clearPlugins();
- int iPluginTry;
- QMap< QString, PluginFetchMeta > qmPluginFetchMeta;
- QString qsSystemPlugins;
- QString qsUserPlugins;
-#ifdef Q_OS_WIN
- HANDLE hToken;
- TOKEN_PRIVILEGES tpPrevious;
- DWORD cbPrevious;
-#endif
-public:
- std::string ssContext, ssContextSent;
- std::wstring swsIdentity, swsIdentitySent;
- bool bValid;
- bool bUnlink;
- float fPosition[3], fFront[3], fTop[3];
- float fCameraPosition[3], fCameraFront[3], fCameraTop[3];
-
- Plugins(QObject *p = nullptr);
- ~Plugins() Q_DECL_OVERRIDE;
-public slots:
- void on_Timer_timeout();
- void rescanPlugins();
- bool fetch();
- void checkUpdates();
- void fetchedUpdatePAPlugins(QByteArray, QUrl);
- void fetchedPAPluginDL(QByteArray, QUrl);
-};
-
-#endif
diff --git a/src/mumble/PositionalData.cpp b/src/mumble/PositionalData.cpp
new file mode 100644
index 000000000..8a510e1df
--- /dev/null
+++ b/src/mumble/PositionalData.cpp
@@ -0,0 +1,242 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "PositionalData.h"
+
+#include <cmath>
+#include <cstdlib>
+#include <stdexcept>
+
+#include <QtCore/QReadLocker>
+
+Vector3D::Vector3D() : x(0.0f), y(0.0f), z(0.0f) {
+}
+
+Vector3D::Vector3D(float x, float y, float z) : x(x), y(y), z(z) {
+}
+
+Vector3D::Vector3D(const Vector3D& other) : x(other.x), y(other.y), z(other.z) {
+}
+
+Vector3D::~Vector3D() {
+}
+
+float Vector3D::operator[](Coord coord) const {
+ switch(coord) {
+ case Coord::X:
+ return x;
+ case Coord::Y:
+ return y;
+ case Coord::Z:
+ return z;
+ }
+
+ // invalid index
+ throw std::out_of_range("May only access x, y or z");
+}
+
+Vector3D Vector3D::operator*(float factor) const {
+ return { x * factor, y * factor, z * factor };
+}
+
+Vector3D Vector3D::operator/(float divisor) const {
+ return { x / divisor, y / divisor, z / divisor };
+}
+
+void Vector3D::operator*=(float factor) {
+ x *= factor;
+ y *= factor;
+ z *= factor;
+}
+
+void Vector3D::operator/=(float divisor) {
+ x /= divisor;
+ y /= divisor;
+ z /= divisor;
+}
+
+bool Vector3D::operator==(const Vector3D& other) const {
+ return equals(other, 0.0f);
+}
+
+Vector3D Vector3D::operator-(const Vector3D& other) const {
+ return { x - other.x, y - other.y, z - other.z };
+}
+
+Vector3D Vector3D::operator+(const Vector3D& other) const {
+ return { x + other.x, y + other.y, z + other.z };
+}
+
+float Vector3D::normSquared() const {
+ return x * x + y * y + z * z;
+}
+
+float Vector3D::norm() const {
+ return std::sqrt(normSquared());
+}
+
+float Vector3D::dotProduct(const Vector3D& other) const {
+ return x * other.x + y * other.y + z * other.z;
+}
+
+Vector3D Vector3D::crossProduct(const Vector3D& other) const {
+ return { y * other.z - z * other.y, z * other.x - x * other.z, x * other.y - y * other.x };
+}
+
+bool Vector3D::equals(const Vector3D& other, float threshold) const {
+ if (threshold == 0.0f) {
+ return x == other.x && y == other.y && z == other.z;
+ } else {
+ threshold = std::abs(threshold);
+
+ return std::abs(x - other.x) < threshold && std::abs(y - other.y) < threshold && std::abs(z - other.z) < threshold;
+ }
+}
+
+bool Vector3D::isZero(float threshold) const {
+ if (threshold == 0.0f) {
+ return x == 0.0f && y == 0.0f && z == 0.0f;
+ } else {
+ return std::abs(x) < threshold && std::abs(y) < threshold && std::abs(z) < threshold;
+ }
+}
+
+void Vector3D::normalize() {
+ float len = norm();
+
+ x /= len;
+ y /= len;
+ z /= len;
+}
+
+void Vector3D::toZero() {
+ x = 0.0f;
+ y = 0.0f;
+ z = 0.0f;
+}
+
+PositionalData::PositionalData()
+ : m_playerPos(),
+ m_playerDir(),
+ m_playerAxis(),
+ m_cameraPos(),
+ m_cameraDir(),
+ m_cameraAxis(),
+ m_context(),
+ m_identity(),
+ m_lock(QReadWriteLock::NonRecursive) {
+}
+
+PositionalData::PositionalData(Position3D playerPos, Vector3D playerDir, Vector3D playerAxis, Position3D cameraPos,
+ Vector3D cameraDir, Vector3D cameraAxis, QString context, QString identity)
+ : m_playerPos(playerPos),
+ m_playerDir(playerDir),
+ m_playerAxis(playerAxis),
+ m_cameraPos(cameraPos),
+ m_cameraDir(cameraDir),
+ m_cameraAxis(cameraAxis),
+ m_context(context),
+ m_identity(identity),
+ m_lock(QReadWriteLock::NonRecursive) {
+}
+
+PositionalData::~PositionalData() {
+}
+
+
+void PositionalData::getPlayerPos(Position3D& pos) const {
+ QReadLocker lock(&m_lock);
+
+ pos = m_playerPos;
+}
+
+Position3D PositionalData::getPlayerPos() const {
+ QReadLocker lock(&m_lock);
+
+ return m_playerPos;
+}
+
+void PositionalData::getPlayerDir(Vector3D& vec) const {
+ QReadLocker lock(&m_lock);
+
+ vec = m_playerDir;
+}
+
+Vector3D PositionalData::getPlayerDir() const {
+ QReadLocker lock(&m_lock);
+
+ return m_playerDir;
+}
+
+void PositionalData::getPlayerAxis(Vector3D& vec) const {
+ QReadLocker lock(&m_lock);
+
+ vec = m_playerAxis;
+}
+
+Vector3D PositionalData::getPlayerAxis() const {
+ QReadLocker lock(&m_lock);
+
+ return m_playerAxis;
+}
+
+void PositionalData::getCameraPos(Position3D& pos) const {
+ QReadLocker lock(&m_lock);
+
+ pos = m_cameraPos;
+}
+
+Position3D PositionalData::getCameraPos() const {
+ QReadLocker lock(&m_lock);
+
+ return m_cameraPos;
+}
+
+void PositionalData::getCameraDir(Vector3D& vec) const {
+ QReadLocker lock(&m_lock);
+
+ vec = m_cameraDir;
+}
+
+Vector3D PositionalData::getCameraDir() const {
+ QReadLocker lock(&m_lock);
+
+ return m_cameraDir;
+}
+
+void PositionalData::getCameraAxis(Vector3D& vec) const {
+ QReadLocker lock(&m_lock);
+
+ vec = m_cameraAxis;
+}
+
+Vector3D PositionalData::getCameraAxis() const {
+ QReadLocker lock(&m_lock);
+
+ return m_cameraAxis;
+}
+
+QString PositionalData::getPlayerIdentity() const {
+ QReadLocker lock(&m_lock);
+
+ return m_identity;
+}
+
+QString PositionalData::getContext() const {
+ QReadLocker lock(&m_lock);
+
+ return m_context;
+}
+
+void PositionalData::reset() {
+ m_playerPos.toZero();
+ m_playerDir.toZero();
+ m_playerAxis.toZero();
+ m_cameraPos.toZero();
+ m_cameraDir.toZero();
+ m_cameraAxis.toZero();
+ m_context = QString();
+ m_identity = QString();
+}
diff --git a/src/mumble/PositionalData.h b/src/mumble/PositionalData.h
new file mode 100644
index 000000000..a0f6f4013
--- /dev/null
+++ b/src/mumble/PositionalData.h
@@ -0,0 +1,171 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_POSITIONAL_AUDIO_CONTEXT_H_
+#define MUMBLE_MUMBLE_POSITIONAL_AUDIO_CONTEXT_H_
+
+#include <QtCore/QString>
+#include <QtCore/QReadWriteLock>
+
+/// An enum for the three cartesian coordinate axes x, y and z
+enum class Coord {X=0,Y,Z};
+
+/// A 3D vector class holding an x-, y- and z-coordinate
+struct Vector3D {
+ /// The vector's x-coordinate
+ float x;
+ /// The vector's y-coordinate
+ float y;
+ /// The vector's z-coordinate
+ float z;
+
+ /// Access the respective coordinate in an array-like fashion
+ ///
+ /// @param coord The Coord to access
+ /// @returns The value of the respective coordinate
+ float operator[](Coord coord) const;
+ /// @param factor The factor to scale by
+ /// @returns A new vector that has been created by scaling this vector by the given factor
+ Vector3D operator*(float factor) const;
+ /// @param divisor The divisor to apply to all coordinates
+ /// @returns A new vector obtained from this one by applying the divisor to all coordinates
+ Vector3D operator/(float divisor) const;
+ /// Scales this vector by the given factor
+ ///
+ /// @param factor The factor to use
+ void operator*=(float factor);
+ /// Divides all of this vector's coordinates by the given divisor
+ ///
+ /// @param divisor The divisor to use
+ void operator/=(float divisor);
+ /// @param other The vector to compare this one to
+ /// @returns Whether the given vector is equal to this one (their coordinates are the same)
+ bool operator==(const Vector3D& other) const;
+ /// @param other The vector to subtract from this one
+ /// @returns A new vector representing the difference of this vector and the other one
+ Vector3D operator-(const Vector3D& other) const;
+ /// @param other The vector to add to this one
+ /// @returns A new vector representing the sum of this vector and the other one
+ Vector3D operator+(const Vector3D& other) const;
+ /// @param other The vector to copy
+ /// @returns A copy of the other vector
+ Vector3D& operator=(const Vector3D& other) = default;
+
+ // allow explicit conversions from this struct to a float-array / float-pointer
+ /// Explicit conversion to a float-array (of length 3) containing the coordinates of this vector
+ explicit operator const float*() const { return &x; };
+ /// Explicit conversion to a float-array (of length 3) containing the coordinates of this vector
+ explicit operator float*() { return &x; };
+
+ /// Default constructor - sets all coordinates to 0
+ Vector3D();
+ /// @param x The x-coordinate
+ /// @param y The y-coordinate
+ /// @param z The z-coordinate
+ Vector3D(float x, float y, float z);
+ /// Copy constructor
+ ///
+ /// @param other The vector to copy
+ Vector3D(const Vector3D& other);
+ /// Destructor
+ ~Vector3D();
+ /// @returns The squared euclidean norm (length of the vector)
+ float normSquared() const;
+ /// If possible normSquared() should be preferred as this doesn't require a square-root operator
+ ///
+ /// @returns The euclidean norm (length of the vector)
+ float norm() const;
+ /// @param other The vector to calculate the dot-product with
+ /// @returns The dot-product between this vector an the other one
+ float dotProduct(const Vector3D& other) const;
+ /// @param other The vector to calculate the cross-product (vector-product) with
+ /// @returns The vector resulting from the cross-product (vector-product)
+ Vector3D crossProduct(const Vector3D& other) const;
+ /// @param other The vector to compare this one to
+ /// @param threshold The maximum absolute difference for coordinates to still be considered equal
+ /// @returns Whether this and the given vector are equal
+ bool equals(const Vector3D& other, float threshold = 0.0f) const;
+ /// @param threshold The maximum absolute value a coordinate may have to still be considered zero
+ /// @returns Whether this vector is the zero-vector
+ bool isZero(float threshold = 0.0f) const;
+ /// Normalizes this vector to a unit-vector. Callin this function on a zero-vector results in undefined behaviour!
+ void normalize();
+ /// Transforms this vector to a zero-vector by setting all coordinates to zero
+ void toZero();
+};
+
+// As we're casting the vector struct to float-arrays, we have to make sure that the compiler won't introduce any padding
+// into the structure
+static_assert(sizeof(Vector3D) == 3*sizeof(float), "The compiler added padding to the Vector3D structure so it can't be cast to a float-array!");
+
+/// A convenient alias as a position can be treated the same way a vector can
+typedef Vector3D Position3D;
+
+
+/// A class holding positional data used in the positional audio feature
+class PositionalData {
+ friend class PluginManager; // needed in order for PluginManager::fetch to write to the contained fields
+ protected:
+ /// The player's position in the 3D world
+ Position3D m_playerPos;
+ /// The direction in which the player is looking
+ Vector3D m_playerDir;
+ /// The connection vector between the player's feet and his/her head
+ Vector3D m_playerAxis;
+ /// The camera's position un the 3D world
+ Position3D m_cameraPos;
+ /// The direction in which the camera is looking
+ Vector3D m_cameraDir;
+ /// The connection from the camera's bottom to its top
+ Vector3D m_cameraAxis;
+ /// The context of this positional data. This might include the game's name, the server currently connected to, etc. and is used
+ /// to determine which players can hear one another
+ QString m_context;
+ /// The player's ingame identity (name)
+ QString m_identity;
+ /// The lock guarding all fields of this class
+ mutable QReadWriteLock m_lock;
+
+ public:
+ /// Default constructor
+ PositionalData();
+ /// Constructor initializing all fields to a specific value
+ PositionalData(Position3D playerPos, Vector3D playerDir, Vector3D playerAxis, Position3D cameraPos, Vector3D cameraDir,
+ Vector3D cameraAxis, QString context, QString identity);
+ /// Destructor
+ ~PositionalData();
+ /// @param[out] pos The player's 3D position
+ void getPlayerPos(Position3D& pos) const;
+ /// @returns The player's 3D position
+ Position3D getPlayerPos() const;
+ /// @param[out] vec The direction in which the player is currently looking
+ void getPlayerDir(Vector3D& vec) const;
+ /// @returns The direction in which the player is currently looking
+ Vector3D getPlayerDir() const;
+ /// @param[out] axis The connection between the player's feet and his/her head
+ void getPlayerAxis(Vector3D& axis) const;
+ /// @returns The connection between the player's feet and his/her head
+ Vector3D getPlayerAxis() const;
+ /// @param[out] pos The camera's 3D position
+ void getCameraPos(Position3D& pos) const;
+ /// @returns The camera's 3D position
+ Position3D getCameraPos() const;
+ /// @param[out] vec The direction in which the camera is currently looking
+ void getCameraDir(Vector3D& vec) const;
+ /// @returns The direction in which the camera is currently looking
+ Vector3D getCameraDir() const;
+ /// @param[out] axis The connection between the player's feet and his/her head
+ void getCameraAxis(Vector3D& axis) const;
+ /// @returns The connection between the player's feet and his/her head
+ Vector3D getCameraAxis() const;
+ /// @returns The player's identity
+ QString getPlayerIdentity() const;
+ /// @returns The current context
+ QString getContext() const;
+ /// Resets all fields in this object
+ void reset();
+};
+
+#endif
diff --git a/src/mumble/ServerHandler.cpp b/src/mumble/ServerHandler.cpp
index 1e7291aac..429adc3f3 100644
--- a/src/mumble/ServerHandler.cpp
+++ b/src/mumble/ServerHandler.cpp
@@ -57,6 +57,10 @@
# include <sys/socket.h>
#endif
+// Init ServerHandler::nextConnectionID
+int ServerHandler::nextConnectionID = -1;
+QMutex ServerHandler::nextConnectionIDMutex(QMutex::Recursive);
+
ServerHandlerMessageEvent::ServerHandlerMessageEvent(const QByteArray &msg, unsigned int mtype, bool flush)
: QEvent(static_cast< QEvent::Type >(SERVERSEND_EVENT)) {
qbaMsg = msg;
@@ -109,6 +113,13 @@ ServerHandler::ServerHandler() : database(new Database(QLatin1String("ServerHand
uiVersion = 0;
iInFlightTCPPings = 0;
+ // assign connection ID
+ {
+ QMutexLocker lock(&nextConnectionIDMutex);
+ nextConnectionID++;
+ connectionID = nextConnectionID;
+ }
+
// Historically, the qWarning line below initialized OpenSSL for us.
// It used to have this comment:
//
@@ -177,6 +188,10 @@ void ServerHandler::customEvent(QEvent *evt) {
}
}
+int ServerHandler::getConnectionID() const {
+ return connectionID;
+}
+
void ServerHandler::udpReady() {
const unsigned int UDP_MAX_SIZE = 2048;
while (qusUdp->hasPendingDatagrams()) {
@@ -683,6 +698,9 @@ void ServerHandler::serverConnectionClosed(QAbstractSocket::SocketError err, con
}
}
+ // Having 2 signals here that basically fire at the same time is wanted behavior!
+ // See the documentation of "aboutToDisconnect" for an explanation.
+ emit aboutToDisconnect(err, reason);
emit disconnected(err, reason);
exit(0);
diff --git a/src/mumble/ServerHandler.h b/src/mumble/ServerHandler.h
index 5fc29c504..d20833aff 100644
--- a/src/mumble/ServerHandler.h
+++ b/src/mumble/ServerHandler.h
@@ -62,6 +62,9 @@ private:
Database *database;
+ static QMutex nextConnectionIDMutex;
+ static int nextConnectionID;
+
protected:
QString qsHostName;
QString qsUserName;
@@ -70,6 +73,7 @@ protected:
unsigned short usResolvedPort;
bool bUdp;
bool bStrong;
+ int connectionID;
/// Flag indicating whether the server we are currently connected to has
/// finished synchronizing already.
@@ -117,6 +121,7 @@ public:
void getConnectionInfo(QString &host, unsigned short &port, QString &username, QString &pw) const;
bool isStrong() const;
void customEvent(QEvent *evt) Q_DECL_OVERRIDE;
+ int getConnectionID() const;
void sendProtoMessage(const ::google::protobuf::Message &msg, unsigned int msgType);
void sendMessage(const char *data, int len, bool force = false);
@@ -169,6 +174,11 @@ public:
void run() Q_DECL_OVERRIDE;
signals:
void error(QAbstractSocket::SocketError, QString reason);
+ // This signal is basically the same as disconnected but it will be emitted
+ // *right before* disconnected is emitted. Thus this can be used by slots
+ // that need to block the disconnected signal from being emitted (using a
+ // direct connection) before they're done.
+ void aboutToDisconnect(QAbstractSocket::SocketError, QString reason);
void disconnected(QAbstractSocket::SocketError, QString reason);
void connected();
void pingRequested();
diff --git a/src/mumble/Settings.cpp b/src/mumble/Settings.cpp
index 31d91bc68..aac13c4a9 100644
--- a/src/mumble/Settings.cpp
+++ b/src/mumble/Settings.cpp
@@ -15,6 +15,8 @@
#include <QtCore/QProcessEnvironment>
#include <QtCore/QStandardPaths>
+#include <QtCore/QFileInfo>
+#include <QtCore/QRegularExpression>
#include <QtGui/QImageReader>
#include <QtWidgets/QSystemTrayIcon>
#if QT_VERSION >= QT_VERSION_CHECK(5,9,0)
@@ -287,6 +289,8 @@ Settings::Settings() {
qRegisterMetaType< ShortcutTarget >("ShortcutTarget");
qRegisterMetaTypeStreamOperators< ShortcutTarget >("ShortcutTarget");
qRegisterMetaType< QVariant >("QVariant");
+ qRegisterMetaType< PluginSetting >("PluginSetting");
+ qRegisterMetaTypeStreamOperators< PluginSetting >("PluginSetting");
atTransmit = VAD;
bTransmitPosition = false;
@@ -346,6 +350,7 @@ Settings::Settings() {
bUpdateCheck = true;
bPluginCheck = true;
#endif
+ bPluginAutoUpdate = false;
qsImagePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
@@ -543,7 +548,8 @@ Settings::Settings() {
qmMessages[Log::OtherSelfMute] = Settings::LogConsole;
qmMessages[Log::OtherMutedOther] = Settings::LogConsole;
qmMessages[Log::UserRenamed] = Settings::LogConsole;
-
+ qmMessages[Log::PluginMessage] = Settings::LogConsole;
+
// Default theme
themeName = QLatin1String("Mumble");
themeStyleName = QLatin1String("Lite");
@@ -888,6 +894,7 @@ void Settings::load(QSettings *settings_ptr) {
LOAD(bUpdateCheck, "ui/updatecheck");
LOAD(bPluginCheck, "ui/plugincheck");
+ LOAD(bPluginAutoUpdate, "ui/pluginAutoUpdate");
LOAD(bHideInTray, "ui/hidetray");
LOAD(bStateInTray, "ui/stateintray");
@@ -1002,9 +1009,26 @@ void Settings::load(QSettings *settings_ptr) {
}
settings_ptr->endGroup();
- settings_ptr->beginGroup(QLatin1String("audio/plugins"));
- foreach (const QString &d, settings_ptr->childKeys()) {
- qmPositionalAudioPlugins.insert(d, settings_ptr->value(d, true).toBool());
+ // Plugins
+ settings_ptr->beginGroup(QLatin1String("plugins"));
+ foreach(const QString &pluginKey, settings_ptr->childGroups()) {
+ QString pluginHash;
+
+ if (pluginKey.contains(QLatin1String("_"))) {
+ // The key contains the filename as well as the hash
+ int index = pluginKey.lastIndexOf(QLatin1String("_"));
+ pluginHash = pluginKey.right(pluginKey.size() - index - 1);
+ } else {
+ pluginHash = pluginKey;
+ }
+
+ PluginSetting pluginSettings;
+ pluginSettings.path = settings_ptr->value(pluginKey + QLatin1String("/path")).toString();
+ pluginSettings.allowKeyboardMonitoring = settings_ptr->value(pluginKey + QLatin1String("/allowKeyboardMonitoring")).toBool();
+ pluginSettings.enabled = settings_ptr->value(pluginKey + QLatin1String("/enabled")).toBool();
+ pluginSettings.positionalDataEnabled = settings_ptr->value(pluginKey + QLatin1String("/positionalDataEnabled")).toBool();
+
+ qhPluginSettings.insert(pluginHash, pluginSettings);
}
settings_ptr->endGroup();
@@ -1259,8 +1283,12 @@ void Settings::save() {
SAVE(qsUsername, "ui/username");
SAVE(qsLastServer, "ui/server");
SAVE(ssFilter, "ui/serverfilter");
+#ifndef NO_UPDATE_CHECK
+ // If this flag has been set, we don't load the following settings so we shouldn't overwrite them here either
SAVE(bUpdateCheck, "ui/updatecheck");
SAVE(bPluginCheck, "ui/plugincheck");
+ SAVE(bPluginAutoUpdate, "ui/pluginAutoUpdate");
+#endif
SAVE(bHideInTray, "ui/hidetray");
SAVE(bStateInTray, "ui/stateintray");
SAVE(bUsage, "ui/usage");
@@ -1373,22 +1401,56 @@ void Settings::save() {
settings_ptr->remove(d);
}
settings_ptr->endGroup();
+
+ // Plugins
+ foreach(const QString &pluginHash, qhPluginSettings.keys()) {
+ QString savePath = QString::fromLatin1("plugins/");
+ const PluginSetting settings = qhPluginSettings.value(pluginHash);
+ const QFileInfo info(settings.path);
+ QString baseName = info.baseName(); // Get the filename without file extensions
+ const bool containsNonASCII = baseName.contains(QRegularExpression(QStringLiteral("[^\\x{0000}-\\x{007F}]")));
+
+ if (containsNonASCII || baseName.isEmpty()) {
+ savePath += pluginHash;
+ } else {
+ // Make sure there are no spaces in the name
+ baseName.replace(QLatin1Char(' '), QLatin1Char('_'));
+
+ // Also include the plugin's filename in the savepath in order
+ // to allow for easier identification
+ savePath += baseName + QLatin1String("__") + pluginHash;
+ }
- settings_ptr->beginGroup(QLatin1String("audio/plugins"));
- foreach (const QString &d, qmPositionalAudioPlugins.keys()) {
- bool v = qmPositionalAudioPlugins.value(d);
- if (!v)
- settings_ptr->setValue(d, v);
- else
- settings_ptr->remove(d);
+ settings_ptr->beginGroup(savePath);
+ settings_ptr->setValue(QLatin1String("path"), settings.path);
+ settings_ptr->setValue(QLatin1String("enabled"), settings.enabled);
+ settings_ptr->setValue(QLatin1String("positionalDataEnabled"), settings.positionalDataEnabled);
+ settings_ptr->setValue(QLatin1String("allowKeyboardMonitoring"), settings.allowKeyboardMonitoring);
+ settings_ptr->endGroup();
}
- settings_ptr->endGroup();
+
settings_ptr->beginGroup(QLatin1String("overlay"));
os.save(settings_ptr);
settings_ptr->endGroup();
}
+QDataStream& operator>>(QDataStream &arch, PluginSetting &setting) {
+ arch >> setting.enabled;
+ arch >> setting.positionalDataEnabled;
+ arch >> setting.allowKeyboardMonitoring;
+
+ return arch;
+}
+
+QDataStream& operator<<(QDataStream &arch, const PluginSetting &setting) {
+ arch << setting.enabled;
+ arch << setting.positionalDataEnabled;
+ arch << setting.allowKeyboardMonitoring;
+
+ return arch;
+}
+
#undef LOAD
#undef LOADENUM
#undef LOADFLAG
diff --git a/src/mumble/Settings.h b/src/mumble/Settings.h
index a074dd6ad..ab46e5391 100644
--- a/src/mumble/Settings.h
+++ b/src/mumble/Settings.h
@@ -59,6 +59,17 @@ QDataStream &operator<<(QDataStream &, const ShortcutTarget &);
QDataStream &operator>>(QDataStream &, ShortcutTarget &);
Q_DECLARE_METATYPE(ShortcutTarget)
+struct PluginSetting {
+ QString path;
+ bool enabled;
+ bool positionalDataEnabled;
+ bool allowKeyboardMonitoring;
+};
+QDataStream& operator>>(QDataStream &arch, PluginSetting &setting);
+QDataStream& operator<<(QDataStream &arch, const PluginSetting &setting);
+Q_DECLARE_METATYPE(PluginSetting);
+
+
struct OverlaySettings {
enum OverlayPresets { AvatarAndName, LargeSquareAvatar };
@@ -249,7 +260,9 @@ struct Settings {
bool bPositionalAudio;
bool bPositionalHeadphone;
float fAudioMinDistance, fAudioMaxDistance, fAudioMaxDistVolume, fAudioBloom;
- QMap< QString, bool > qmPositionalAudioPlugins;
+ /// Contains the settings for each individual plugin. The key in this map is the Hex-represented SHA-1
+ /// hash of the plugin's UTF-8 encoded absolute file-path on the hard-drive.
+ QHash< QString, PluginSetting > qhPluginSettings;
OverlaySettings os;
@@ -351,6 +364,7 @@ struct Settings {
bool bUpdateCheck;
bool bPluginCheck;
+ bool bPluginAutoUpdate;
// PTT Button window
bool bShowPTTButtonWindow;
diff --git a/src/mumble/UserModel.cpp b/src/mumble/UserModel.cpp
index 35fca925d..ceb7c1dd4 100644
--- a/src/mumble/UserModel.cpp
+++ b/src/mumble/UserModel.cpp
@@ -1052,6 +1052,8 @@ ClientUser *UserModel::addUser(unsigned int id, const QString &name) {
updateOverlay();
+ emit userAdded(p->uiSession);
+
return p;
}
@@ -1087,6 +1089,8 @@ void UserModel::removeUser(ClientUser *p) {
updateOverlay();
+ emit userRemoved(p->uiSession);
+
delete p;
delete item;
}
@@ -1303,6 +1307,8 @@ void UserModel::renameChannel(Channel *c, const QString &name) {
moveItem(pi, pi, item);
}
+
+ emit channelRenamed(c->iId);
}
void UserModel::repositionChannel(Channel *c, const int position) {
@@ -1341,6 +1347,9 @@ Channel *UserModel::addChannel(int id, Channel *p, const QString &name) {
if (Global::get().s.ceExpand == Settings::AllChannels)
Global::get().mw->qtvUsers->setExpanded(index(item), true);
+
+ emit channelAdded(c->iId);
+
return c;
}
@@ -1501,6 +1510,8 @@ bool UserModel::removeChannel(Channel *c, const bool onlyIfUnoccupied) {
Channel::remove(c);
+ emit channelRemoved(c->iId);
+
delete item;
delete c;
return true;
diff --git a/src/mumble/UserModel.h b/src/mumble/UserModel.h
index c9824f25a..330500509 100644
--- a/src/mumble/UserModel.h
+++ b/src/mumble/UserModel.h
@@ -213,6 +213,27 @@ public slots:
void recheckLinks();
void updateOverlay() const;
void toggleChannelFiltered(Channel *c);
+signals:
+ /// A signal emitted whenever a user is added to the model.
+ ///
+ /// @param userSessionID The ID of that user's session
+ void userAdded(unsigned int userSessionID);
+ /// A signal emitted whenever a user is removed from the model.
+ ///
+ /// @param userSessionID The ID of that user's session
+ void userRemoved(unsigned int userSessionID);
+ /// A signal that emitted whenever a channel is added to the model.
+ ///
+ /// @param channelID The ID of the channel
+ void channelAdded(int channelID);
+ /// A signal that emitted whenever a channel is removed from the model.
+ ///
+ /// @param channelID The ID of the channel
+ void channelRemoved(int channelID);
+ /// A signal that emitted whenever a channel is renamed.
+ ///
+ /// @param channelID The ID of the channel
+ void channelRenamed(int channelID);
};
#endif
diff --git a/src/mumble/main.cpp b/src/mumble/main.cpp
index dad3cce16..a1ef34826 100644
--- a/src/mumble/main.cpp
+++ b/src/mumble/main.cpp
@@ -11,12 +11,13 @@
#include "AudioWizard.h"
#include "Cert.h"
#include "Database.h"
+#include "Log.h"
+#include "LogEmitter.h"
#include "DeveloperConsole.h"
#include "LCD.h"
#include "Log.h"
#include "LogEmitter.h"
#include "MainWindow.h"
-#include "Plugins.h"
#include "ServerHandler.h"
#ifdef USE_ZEROCONF
# include "Zeroconf.h"
@@ -43,6 +44,9 @@
#include "Themes.h"
#include "UserLockFile.h"
#include "VersionCheck.h"
+#include "PluginInstaller.h"
+#include "PluginManager.h"
+#include "Global.h"
#include <QtCore/QProcess>
#include <QtGui/QDesktopServices>
@@ -58,7 +62,6 @@
# include <shellapi.h>
#endif
-#include "Global.h"
#ifdef BOOST_NO_EXCEPTIONS
namespace boost {
@@ -229,6 +232,7 @@ int main(int argc, char **argv) {
QStringList extraTranslationDirs;
QString localeOverwrite;
+ QStringList pluginsToBeInstalled;
if (a.arguments().count() > 1) {
for (int i = 1; i < args.count(); ++i) {
if (args.at(i) == QLatin1String("-h") || args.at(i) == QLatin1String("--help")
@@ -237,13 +241,15 @@ int main(int argc, char **argv) {
#endif
) {
QString helpMessage =
- MainWindow::tr("Usage: mumble [options] [<url>]\n"
+ MainWindow::tr("Usage: mumble [options] [<url> | <plugin_list>]\n"
"\n"
"<url> specifies a URL to connect to after startup instead of showing\n"
"the connection window, and has the following form:\n"
"mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/"
"<subchannel>...]][?version=<x.y.z>]\n"
"\n"
+ "<plugin_list> is a list of plugin files that shall be installed"
+ "\n"
"The version query parameter has to be set in order to invoke the\n"
"correct client version. It currently defaults to 1.2.0.\n"
"\n"
@@ -399,14 +405,18 @@ int main(int argc, char **argv) {
return 1;
}
} else {
- if (!bRpcMode) {
- QUrl u = QUrl::fromEncoded(args.at(i).toUtf8());
- if (u.isValid() && (u.scheme() == QLatin1String("mumble"))) {
- url = u;
- } else {
- QFile f(args.at(i));
- if (f.exists()) {
- url = QUrl::fromLocalFile(f.fileName());
+ if (PluginInstaller::canBePluginFile(args.at(i))) {
+ pluginsToBeInstalled << args.at(i);
+ } else {
+ if (!bRpcMode) {
+ QUrl u = QUrl::fromEncoded(args.at(i).toUtf8());
+ if (u.isValid() && (u.scheme() == QLatin1String("mumble"))) {
+ url = u;
+ } else {
+ QFile f(args.at(i));
+ if (f.exists()) {
+ url = QUrl::fromLocalFile(f.fileName());
+ }
}
}
}
@@ -583,6 +593,22 @@ int main(int argc, char **argv) {
Global::get().s.qsLanguage = settingsLocale.nativeLanguageName();
}
+ if (!pluginsToBeInstalled.isEmpty()) {
+
+ foreach(QString currentPlugin, pluginsToBeInstalled) {
+
+ try {
+ PluginInstaller installer(currentPlugin);
+ installer.exec();
+ } catch(const PluginInstallException& e) {
+ qCritical() << qUtf8Printable(e.getMessage());
+ }
+
+ }
+
+ return 0;
+ }
+
qWarning("Locale is \"%s\" (System: \"%s\")", qUtf8Printable(settingsLocale.name()), qUtf8Printable(systemLocale.name()));
Mumble::Translations::LifetimeGuard translationGuard = Mumble::Translations::installTranslators(settingsLocale, a, extraTranslationDirs);
@@ -605,6 +631,10 @@ int main(int argc, char **argv) {
// Initialize zeroconf
Global::get().zeroconf = new Zeroconf();
#endif
+
+ // PluginManager
+ Global::get().pluginManager = new PluginManager();
+ Global::get().pluginManager->rescanPlugins();
#ifdef USE_OVERLAY
Global::get().o = new Overlay();
@@ -661,10 +691,6 @@ int main(int argc, char **argv) {
Global::get().l->log(Log::Information, MainWindow::tr("Welcome to Mumble."));
- // Plugins
- Global::get().p = new Plugins(nullptr);
- Global::get().p->rescanPlugins();
-
Audio::start();
a.setQuitOnLastWindowClosed(false);
@@ -736,12 +762,13 @@ int main(int argc, char **argv) {
new VersionCheck(false, Global::get().mw, true);
# endif
}
-#else
- Global::get().mw->msgBox(MainWindow::tr("Skipping version check in debug mode."));
-#endif
+
if (Global::get().s.bPluginCheck) {
- Global::get().p->checkUpdates();
+ Global::get().pluginManager->checkForPluginUpdates();
}
+#else // QT_NO_DEBUG
+ Global::get().mw->msgBox(MainWindow::tr("Skipping version check in debug mode."));
+#endif // QT_NO_DEBUG
if (url.isValid()) {
OpenURLEvent *oue = new OpenURLEvent(url);
@@ -772,8 +799,20 @@ int main(int argc, char **argv) {
// Wait for the ServerHandler thread to exit before proceeding shutting down. This is so that
// all events that the ServerHandler might emit are enqueued into Qt's event loop before we
// ask it to pocess all of them below.
- if (!sh->wait(2000)) {
- qCritical("main: ServerHandler did not exit within specified time interval");
+
+ // We iteratively probe whether the ServerHandler thread has finished yet. If it did
+ // not, we execute pending events in the main loop. This is because the ServerHandler
+ // could be stuck waiting for a function to complete in the main loop (e.g. a plugin
+ // uses the API in the disconnect callback).
+ // We assume that this entire process is done in way under a second.
+ int iterations = 0;
+ while (!sh->wait(10)) {
+ QCoreApplication::processEvents();
+ iterations++;
+
+ if (iterations > 200) {
+ qFatal("ServerHandler does not exit as expected");
+ }
}
}
@@ -785,20 +824,26 @@ int main(int argc, char **argv) {
delete srpc;
+ delete Global::get().talkingUI;
+ // Delete the MainWindow before the ServerHandler gets reset in order to allow all callbacks
+ // trggered by this deletion to still access the ServerHandler (atm all these callbacks are in PluginManager.cpp)
+ delete Global::get().mw;
+ Global::get().mw = nullptr; // Make it clear to any destruction code, that MainWindow no longer exists
+
Global::get().sh.reset();
- while (sh && !sh.unique())
+
+ while (sh && ! sh.unique())
QThread::yieldCurrentThread();
sh.reset();
- delete Global::get().talkingUI;
- delete Global::get().mw;
-
delete Global::get().nam;
delete Global::get().lcd;
delete Global::get().db;
- delete Global::get().p;
delete Global::get().l;
+ Global::get().l = nullptr; // Make it clear to any destruction code that Log no longer exists
+
+ delete Global::get().pluginManager;
#ifdef USE_ZEROCONF
delete Global::get().zeroconf;
diff --git a/src/mumble/mumble_ar.ts b/src/mumble/mumble_ar.ts
index 71cf51bd8..4fc00bb4e 100644
--- a/src/mumble/mumble_ar.ts
+++ b/src/mumble/mumble_ar.ts
@@ -3782,6 +3782,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation>المستخدم توقف عن الاستماع الى القناة</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6125,12 +6129,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6450,10 +6455,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7028,30 +7037,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">اسم</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_bg.ts b/src/mumble/mumble_bg.ts
index 4f3c1690e..8ff93bd2e 100644
--- a/src/mumble/mumble_bg.ts
+++ b/src/mumble/mumble_bg.ts
@@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6122,12 +6126,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6447,10 +6452,6 @@ Valid options are:
<translation>Свързване към последния сървър при пускане на програмата</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Сваляне на обновления за слоя и приставките при пускане на програмата</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Поверителност</translation>
</message>
@@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Име</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Включено</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_br.ts b/src/mumble/mumble_br.ts
index 5fed8a97e..56c3e4cd4 100644
--- a/src/mumble/mumble_br.ts
+++ b/src/mumble/mumble_br.ts
@@ -3778,6 +3778,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6121,12 +6125,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6446,10 +6451,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6474,6 +6475,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7024,30 +7033,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Anv</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_ca.ts b/src/mumble/mumble_ca.ts
index d545cbe6a..df853080e 100644
--- a/src/mumble/mumble_ca.ts
+++ b/src/mumble/mumble_ca.ts
@@ -3784,6 +3784,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6127,12 +6131,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6452,10 +6457,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6480,6 +6481,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7030,30 +7039,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Nom</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_cs.ts b/src/mumble/mumble_cs.ts
index df4b57b60..d564ee872 100644
--- a/src/mumble/mumble_cs.ts
+++ b/src/mumble/mumble_cs.ts
@@ -3833,6 +3833,10 @@ Toto pole popisuje velikost LCD zařízení. Velikost je udávána buď v pixele
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6181,12 +6185,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6506,10 +6511,6 @@ Valid options are:
<translation>Při startu se znovu připojit na poslední server</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Při startu stáhnout aktualizace zásuvných modulů a překryvů</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6534,6 +6535,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7088,31 +7097,189 @@ Pro aktualizaci těchto souborů na jejich poslední verzi, klikněte na tlačí
<translation>Jméno</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Povoleno</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>V režimu ladění přeskakuji aktualizaci zásuvných modulů.</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Aktualizován nebo stáhnut nový zásuvný modul do %1.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Nelze instalovat nový zásuvný modul do %1.</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 ztraceno propojení.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 propojen.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_cy.ts b/src/mumble/mumble_cy.ts
index 19d907b49..d3ce85dca 100644
--- a/src/mumble/mumble_cy.ts
+++ b/src/mumble/mumble_cy.ts
@@ -3782,6 +3782,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6125,12 +6129,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6450,10 +6455,6 @@ Valid options are:
<translation>Ailgysylltu i&apos;r gweinydd olaf ar gychwyn</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7028,30 +7037,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Enw</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Wedi&apos;i alluogi</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_da.ts b/src/mumble/mumble_da.ts
index c480f2470..72d08e75f 100644
--- a/src/mumble/mumble_da.ts
+++ b/src/mumble/mumble_da.ts
@@ -3831,6 +3831,10 @@ Dette felt beskriver størrelsen af en LCD-enhed. Størrelsen er enten opgivet i
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6177,12 +6181,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6502,10 +6507,6 @@ Valid options are:
<translation>Forbind igen til sidst anvendte server ved opstart</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Download plugin- og overlægningsopdateringer ved opstart</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6530,6 +6531,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7084,31 +7093,189 @@ For at opgradere disse filer til deres nyeste version, klik på knappen nedenfor
<translation>Navn</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Aktiveret</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Springer over pluginopdateringer i fejlfindingstilstand.</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Downloadede nyt eller opdateret plugin til %1.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Installation af nyt plugin til %1 mislykkedes.</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 mistede link.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 er linket.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_de.ts b/src/mumble/mumble_de.ts
index 5ac64fbe7..3ceee3768 100644
--- a/src/mumble/mumble_de.ts
+++ b/src/mumble/mumble_de.ts
@@ -3882,6 +3882,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation>Benutzer hört dem Kanal nicht mehr zu</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6239,12 +6243,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6567,10 +6572,6 @@ Mumble hat ein kleines Entwickler-Team. Deshalb muss die verfügbare Zeit auf di
<translation>Beim Start zum zuletzt benutzten Server verbinden</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Plugin und Overlay Updates beim Starten herunterladen</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Datenschutz</translation>
</message>
@@ -6596,6 +6597,14 @@ Verhindert, dass potenziell identifizierende Informationen über das Betriebssys
<source>Hide public server list</source>
<translation>Öffentliche Serverliste verstecken</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7151,31 +7160,189 @@ Um diese Dateien zu aktualisieren, klicken Sie unten den Button.</translation>
<translation>Name</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Aktiviert</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Aktivieren</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Überspringe Plugin-Aktualisierung im Debug-Modus.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Neues oder aktualisiertes Plugin nach %1 heruntergeladen.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Installation eines neuen Plugins nach %1 fehlgeschlagen.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 hat Verbindung verloren.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 verbunden.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_el.ts b/src/mumble/mumble_el.ts
index a577a1e36..761c641a3 100644
--- a/src/mumble/mumble_el.ts
+++ b/src/mumble/mumble_el.ts
@@ -3835,6 +3835,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6186,12 +6190,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6511,10 +6516,6 @@ Valid options are:
<translation>Επανασύνδεση με τον τελευταίο διακομιστή που χρησιμοποιήθηκε κατά την εκκίνηση.</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Να γίνεται λήψη ενημερώσεων για τα πρόσθετα και το overlay κατά την εκκίνηση</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Ιδιωτικότητα</translation>
</message>
@@ -6540,6 +6541,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7094,31 +7103,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Όνομα</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Ενεργοποιημένο</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Παράλειψη της ενημέρωσης των πρόσθετων στη λειτουργία εντοπισμού σφαλμάτων.</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Έγινε λήψη νέου ή ενημερωμένου πρόσθετου στο %1.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Απέτυχε η εγκατάσταση του νέου πρόσθετου στο %1.</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 χαμένος σύνδεσμος.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 σύνδεσμος.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_en.ts b/src/mumble/mumble_en.ts
index 4a0358e0d..49e2aceb2 100644
--- a/src/mumble/mumble_en.ts
+++ b/src/mumble/mumble_en.ts
@@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6120,12 +6124,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6445,10 +6450,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished"></translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_en_GB.ts b/src/mumble/mumble_en_GB.ts
index c1cd32e0d..63b1260bb 100644
--- a/src/mumble/mumble_en_GB.ts
+++ b/src/mumble/mumble_en_GB.ts
@@ -3814,6 +3814,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6157,12 +6161,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6482,10 +6487,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6510,6 +6511,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7060,30 +7069,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Name</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_eo.ts b/src/mumble/mumble_eo.ts
index 614e15add..91f0f740a 100644
--- a/src/mumble/mumble_eo.ts
+++ b/src/mumble/mumble_eo.ts
@@ -3786,6 +3786,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6130,12 +6134,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6455,10 +6460,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privateco</translation>
</message>
@@ -6483,6 +6484,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation>Kaŝi la publikan servilaliston</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7033,30 +7042,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Nomo</translation>
</message>
<message>
- <source>Enabled</source>
- <translation type="unfinished">Enŝaltite</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_es.ts b/src/mumble/mumble_es.ts
index 14299e098..9bfdc9a17 100644
--- a/src/mumble/mumble_es.ts
+++ b/src/mumble/mumble_es.ts
@@ -3838,6 +3838,10 @@ Este campo describe el tamaño de un dispositivo LCD. El tamaño se da, o bien e
<source>User stopped listening to channel</source>
<translation>El usuario dejó de escuchar en su canal</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6191,12 +6195,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6516,10 +6521,6 @@ Valid options are:
<translation>Reconectar al último servidor al inicio</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Descargar actualizaciones de los complementos y la superposición al inicio</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privacidad</translation>
</message>
@@ -6545,6 +6546,14 @@ Impide que el cliente envíe información potencialmente identificable sobre el
<source>Hide public server list</source>
<translation>Esconder la lista de servidores públicos</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7099,31 +7108,189 @@ Para actualizar estos ficheros a la última versión, haga clic en el botón inf
<translation>Nombre</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Habilitado</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Habilitar</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Omitiendo la actualización de los complementos en el modo de depuración.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Se ha descargado un complemento nuevo o actualizado para %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>No se pudo instalar un nuevo complemento para %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 perdió el vínculo.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 vinculado.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_et.ts b/src/mumble/mumble_et.ts
index 7277cff3d..8170471b1 100644
--- a/src/mumble/mumble_et.ts
+++ b/src/mumble/mumble_et.ts
@@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6122,12 +6126,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6447,10 +6452,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privaatsus</translation>
</message>
@@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7025,31 +7034,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Nimi</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Lubatud</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Luba</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>%1 lost link</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>%1 linked</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 lingitud.</translation>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_eu.ts b/src/mumble/mumble_eu.ts
index f7038bb97..21a710694 100644
--- a/src/mumble/mumble_eu.ts
+++ b/src/mumble/mumble_eu.ts
@@ -3796,6 +3796,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6141,12 +6145,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6466,10 +6471,6 @@ Valid options are:
<translation>Birkonektatu azken zerbitzarira hastean</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6494,6 +6495,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7044,31 +7053,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Izena</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Gaituta</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>%1 lost link</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Eguneratutako edo berria den gehigarria deskargatuta %1 -era.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>%1-era gehigarri berria instalatzean errorea.</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 lotura galdua.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 lotuta.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_fa_IR.ts b/src/mumble/mumble_fa_IR.ts
index c139066bd..1d1e9b2f4 100644
--- a/src/mumble/mumble_fa_IR.ts
+++ b/src/mumble/mumble_fa_IR.ts
@@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6120,12 +6124,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6445,10 +6450,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">نام</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_fi.ts b/src/mumble/mumble_fi.ts
index 252b08ab2..8948e902c 100644
--- a/src/mumble/mumble_fi.ts
+++ b/src/mumble/mumble_fi.ts
@@ -3838,6 +3838,10 @@ Kenttä kuvaa LCD-laitteen koon. Koko annetaan joko pikseleinä (graafinen LCD)
<source>User stopped listening to channel</source>
<translation>Käyttäjä lopetti kanavan kuuntelun</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6231,12 +6235,13 @@ Päteviä valintoja ovat:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6556,10 +6561,6 @@ Valid options are:
<translation>Yhdistä viimeisimpään palvelimeen käynnistymisen yhteydessä</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Lataa liitännäisten ja overlayn päivitykset ohjelman käynnistymisen yhteydessä</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Yksityisyys</translation>
</message>
@@ -6585,6 +6586,14 @@ Estää mahdollisesti tunnistamista helpottavien tietojen, koskien käyttöjärj
<source>Hide public server list</source>
<translation>Piilota julkisten palvelinten lista</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7139,31 +7148,189 @@ Paina alapuolen napista päivittääksesi Overlayn tiedostot viimeisimpään ver
<translation>Nimi</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Käytössä</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Käytä</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Liitännäispäivitys ohitetaan debug-tilassa.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Ladattiin uusi tai päivitettiin liitännäinnen %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Uuden liitännäisen asennus epäonnistui %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 yhteys kadotettu.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 yhdistetty.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_fr.ts b/src/mumble/mumble_fr.ts
index f559e2ec4..69846001c 100644
--- a/src/mumble/mumble_fr.ts
+++ b/src/mumble/mumble_fr.ts
@@ -3837,6 +3837,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation>L&apos;utilisateur a cessé d&apos;écouter le salon</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6191,12 +6195,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6516,10 +6521,6 @@ Valid options are:
<translation>Se reconnecter au dernier serveur utilisé au démarrage</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Télécharger les mises à jour des plugins et de l&apos;overlay au démarrage</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Confidentialité</translation>
</message>
@@ -6545,6 +6546,14 @@ Empêche le client d&apos;envoyer des informations pouvant identifier le systèm
<source>Hide public server list</source>
<translation>Cacher la liste des serveurs publics</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7099,31 +7108,189 @@ Pour mettre à jour l&apos;overlay, cliquez sur le bouton ci-dessous.</translati
<translation>Nom</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Activé</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Activer</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Passe la mise à jour des plugins en mode débogage.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Téléchargé le nouveau plugin ou mis à jour vers %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Échec de l&apos;installation du nouveau plugin dans %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 est désactivé.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 est activé.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_gl.ts b/src/mumble/mumble_gl.ts
index a18a12f93..efb2ebbd0 100644
--- a/src/mumble/mumble_gl.ts
+++ b/src/mumble/mumble_gl.ts
@@ -3780,6 +3780,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6123,12 +6127,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6448,10 +6453,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6476,6 +6477,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7026,30 +7035,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Nome</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_he.ts b/src/mumble/mumble_he.ts
index df3fd72d2..0e7680463 100644
--- a/src/mumble/mumble_he.ts
+++ b/src/mumble/mumble_he.ts
@@ -3829,6 +3829,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6174,12 +6178,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6499,10 +6504,6 @@ Valid options are:
<translation>התחבר שוב אל שרת אחרון בעת הפעלה</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>הורד עדכונים עבור תוספים וממשק-המשחק</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6527,6 +6528,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7081,32 +7090,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>שם</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>מאופשרת</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>מדלג על עדכון תוספים במצב ניפוי-שגיאות.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>מוריד או מעדכן תוסף עבור %1.
-</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>נכשל בהתקנת תוסף חדש ל-%1.</translation>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 איבד קישור.</translation>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 קושר.</translation>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_hu.ts b/src/mumble/mumble_hu.ts
index d92ca1cc1..0e6933174 100644
--- a/src/mumble/mumble_hu.ts
+++ b/src/mumble/mumble_hu.ts
@@ -3826,6 +3826,10 @@ Ez a mező mutatja egy LCD eszköz méretét. A méret vagy pixelben (a grafikus
<source>User stopped listening to channel</source>
<translation>User stopped listening to channel</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6171,12 +6175,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6496,10 +6501,6 @@ Valid options are:
<translation>Újracsatlakozás az utoljára használt kiszolgálóhoz indításkor</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Bővítmények és képátfedés frissítése indításkor</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Adatvédelem</translation>
</message>
@@ -6525,6 +6526,14 @@ Ez a beállítás meggátolja, hogy a Mumble érzékeny adatokat továbbítson a
<source>Hide public server list</source>
<translation>Nyilvános kiszolgálók listájának elrejtése</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7075,31 +7084,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Név</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Engedélyezett</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Bővítmény frissítésének mellőzése hibakeresési módban.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Új plugin letöltve vagy frissítve: %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Nem sikerült új plugin telepítése: %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 elveszett kapcsolat.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 kapcsolva.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_it.ts b/src/mumble/mumble_it.ts
index 312571797..d178feb5f 100644
--- a/src/mumble/mumble_it.ts
+++ b/src/mumble/mumble_it.ts
@@ -3838,6 +3838,10 @@ Questo campo descrive la dimensione di un dispositivo LCD. La dimensione è espr
<source>User stopped listening to channel</source>
<translation>Un utente ha smesso di ascoltare il tuo canale</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6215,12 +6219,13 @@ Azioni valide:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6270,59 +6275,7 @@ Valid options are:
Otherwise the locale will be permanently saved to
Mumble&apos;s settings.
</source>
- <translation>Utilizzo: mumble [opzioni] [&lt;url&gt;]
-
-&lt;url&gt; specifica un URL a cui connettersi dopo l&apos;avvio invece di visualizzare la finestra di connessione e ha la seguente forma:
-mumble: // [&lt;username&gt; [: &lt;password&gt;] @] &lt;host&gt; [: &lt;port&gt;] [/ &lt;channel&gt; [/ &lt;subchannel&gt; ...]] [? version = &lt;x.y.z&gt;]
-
-Il parametro di query della versione deve essere impostato per richiamare la versione client corretta. Attualmente il valore predefinito è 1.2.0.
-
-Le opzioni valide sono:
- -h, --help Mostra questo testo di aiuto ed esce.
- -m, --multiple
- Consenti l&apos;avvio di più istanze del client.
- -c, --config
- Specificare un file di configurazione alternativo.
- Se lo usi per eseguire più istanze di Mumble contemporaneamente,
- assicurati di impostare un valore &quot;database&quot; alternativo nel file config.
- -n, --noidentity
- Sopprimi il caricamento dei file di identità (ad esempio, certificati).
- -jn, --jackname &lt;arg&gt;
- Imposta il nome del client Jack personalizzato.
- --licenza
- Mostra la licenza di Mumble.
- --autori
- Mostra gli autori di Mumble.
- - licenze di terze parti
- Mostra le licenze per il software di terze parti utilizzato da Mumble.
- --window-title-ext &lt;arg&gt;
- Imposta un&apos;estensione del titolo della finestra personalizzata.
- --dump-input-stream
- Esegui il dump dei flussi PCM in varie parti della catena di input
- (utile per scopi di debug)
- - ingresso microfono grezzo
- - rilettura dell&apos;altoparlante per la cancellazione dell&apos;eco
- - ingresso microfono elaborato
- --print-echocancel-queue
- Stampa su stdout lo stato della coda di cancellazione dell&apos;eco
- (utile per scopi di debug)
- --translation-dir &lt;dir&gt;
- Specifica una traduzione aggiuntiva fir &lt;dir&gt; in cui
- Mumble cercherà i file di traduzione che sovrascrivono
- quelli in bundle
- Le directory aggiunte in questo modo hanno una priorità maggiore delle
- posizioni predefinite utilizzate altrimenti
- --print-translation-dirs
- Stampa i percorsi in cui Mumble cercherà
- file di traduzione che sovrascrivono quelli in bundle.
- (Utile per i traduttori che testano le loro traduzioni)
- --locale &lt;locale&gt;
- Sovrascrivi le impostazioni locali nelle impostazioni di Mumble con un file
- locale che corrisponde alla stringa di locale specificata.
- Se il formato non è valido, Mumble genererà un errore.
- In caso contrario, la locale verrà salvata in modo permanente nelle
- impostazioni di Mumble.
-</translation>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -6592,10 +6545,6 @@ Le opzioni valide sono:
<translation>All&apos;avvio connettiti all&apos;ultimo server visitato</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Aggiorna plugin e sovrapposizione all&apos;avvio</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privacy</translation>
</message>
@@ -6621,6 +6570,14 @@ Previene l&apos;invio da parte del client di informazioni potenzialmente identif
<source>Hide public server list</source>
<translation>Nascondi lista server pubblici</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7175,31 +7132,189 @@ Per aggiornare questi file all&apos;ultima versione, premi il pulsante sottostan
<translation>Nome</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Abilitato</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Abilita</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>I plugin non verranno aggiornati in modalità Debug.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Aggiunto o aggiornato il seguente plugin: %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Installazione del seguente plugin fallita: %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>Collegamento perso con %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>collegato con %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_ja.ts b/src/mumble/mumble_ja.ts
index 00cbf7ba6..add00ec4f 100644
--- a/src/mumble/mumble_ja.ts
+++ b/src/mumble/mumble_ja.ts
@@ -3826,6 +3826,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6170,12 +6174,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6495,10 +6500,6 @@ Valid options are:
<translation>起動時に最後に接続したサーバに再接続する</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>開始時にプラグインとオーバレイの更新をダウンロードする</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6523,6 +6524,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7077,31 +7086,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>名前</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>有効化</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>デバッグモードではプラグインのアップデートはスキップします。</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>更新されたプラグインを %1 にダウンロードしました。</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>%1 に新しいプラグインをインストールできません。</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 はリンクを失いました。</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 はリンクされました。</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_ko.ts b/src/mumble/mumble_ko.ts
index 6cd8662fe..420312974 100644
--- a/src/mumble/mumble_ko.ts
+++ b/src/mumble/mumble_ko.ts
@@ -3809,6 +3809,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6152,12 +6156,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6478,10 +6483,6 @@ Valid options are:
<translation>시작시 마지막으로 접속한 서버에 다시 접속한다</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>시작시 플러그 인과 오버레이의 업데이트를 다운로드한다.</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6506,6 +6507,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7060,31 +7069,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>이름</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>유효</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>디버깅 모드에서는 플러그인의 업데이트가 스킵 됩니다.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>업데이트된 플러그인을 %1에 다운 받았습니다.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>%1에 새로운 플러그인을 설치할 수 없습니다.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1은 링크가 소실 되었습니다.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1은 링크되었습니다.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_lt.ts b/src/mumble/mumble_lt.ts
index abd115c97..4c7d4c039 100644
--- a/src/mumble/mumble_lt.ts
+++ b/src/mumble/mumble_lt.ts
@@ -3809,6 +3809,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6154,12 +6158,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6479,10 +6484,6 @@ Valid options are:
<translation>Paleidus programą, iš naujo prisijungti prie paskutinio serverio</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privatumas</translation>
</message>
@@ -6507,6 +6508,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7061,30 +7070,188 @@ Norėdami naujinti šiuos failus į naujausią versiją, spustelėkite mygtuką
<translation>Pavadinimas</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Įjungta</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Įjungti</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Derinimo veiksenoje, praleidžiamas įskiepio atnaujinimas.</translation>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Atsisiųstas naujas ar atnaujintas įskiepis į %1.</translation>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Nepavyko įdiegti naujo įskiepio į %1.</translation>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_nl.ts b/src/mumble/mumble_nl.ts
index 774988679..894779254 100644
--- a/src/mumble/mumble_nl.ts
+++ b/src/mumble/mumble_nl.ts
@@ -3838,6 +3838,10 @@ Veld beschrijft LCD-apparaatgrootte aangeduid in pixels (voor Grafische LCD&apos
<source>User stopped listening to channel</source>
<translation>Gebruiker luistert niet meer naar kanaal</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6214,12 +6218,13 @@ Valide acties zijn:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6269,60 +6274,7 @@ Valid options are:
Otherwise the locale will be permanently saved to
Mumble&apos;s settings.
</source>
- <translation>Gebruik: mumble [opties] [&lt;url&gt;]
-
-&lt;url&gt; specifieert een URL om mee te verbinden bij het opstarten i.p.v.
-het venster om te verbinden te tonen, en heeft het volgende formaat:
-mumble://[&lt;gebruikersnaam&gt;[:&lt;wachtwoord&gt;]@]&lt;serveradres&gt;[:&lt;poort&gt;][/&lt;kanaal&gt;[/&lt;subkanaal&gt;...]][?versie=&lt;x.y.z&gt;]
-
-De versie-parameter moet ingesteld worden om de correcte versie
-van Mumble te kiezen. Momenteel is dit standaard 1.2.0.
-
-Valide opties zijn:
- -h, --help Toont deze hulptekst en sluit vervolgens af.
- -m, --multiple
- Laat toe dat meerdere instanties van de applicatie tegelijk draaien.
- -c, --config
- Geef een alternatief configuratiebestand op.
- Als je dit gebruikt om meerdere instanties van Mumble tegelijk te draaien,
- vergeet dan niet in dit bestand een aparte waarde voor &apos;database&apos; op te geven.
- -n, --noidentity
- Voorkom het laden van identiteitsbestanden (bv. certificaten).
- -jn, --jackname &lt;argument&gt;
- Stel een zelfgekozen client-naam in voor Jack.
- --license
- Toon de licentie van Mumble.
- --authors
- Toon een overzicht van de auteurs van Mumble.
- --third-party-licenses
- Toon licenties van software van derde partijen die gebruikt wordt door Mumble.
- --window-title-ext &lt;argument&gt;
- Stelt een zelfgekozen achtervoegsel in voor de titel van het venster.
- --dump-input-streams
- Dump PCM streams op verschillende plaatsen gedurende de invoerverwerking
- (nuttig bij het opsporen van bugs)
- - Onverwerkte microfooninvoer
- - Teruglezen van spraak bij echo-opheffing
- - Verwerkte microfooninvoer
- --print-echocancel-queue
- Stuur de staat van de wachtrij die gebruikt wordt voor echo-opheffing naar stdout
- (nuttig bij het opsporen van bugs)
- --translation-dir &lt;map&gt;
- Geeft een bijkomende vertalingsmap &lt;map&gt; op waarin Mumble moet zoeken op
- vertalingsbestanden, die vervolgens voorrang krijgen op de ingebouwde.
- Deze mappen krijgen een hogere prioriteit dan de standaardlocaties die anders
- gebruikt worden.
- --print-translation-dirs
- Print de paden die Mumble doorzoekt voor vertalingsbestanden die voorrang
- krijgen op de ingebouwde vertalingen af.
- (Nuttig voor vertalers die hun vertalingen willen testen)
- --locale &lt;taalgebied&gt;
- Overschrijf het taalgebied uit de instellingen van Mumble met een taalgebied
- dat overeenkomt met de opgegeven identificator.
- Als het formaat ongeldig is, zal Mumble een fout geven.
- Indien er geen fout is, zal dit taalgebied permanent opgeslagen worden in de
- instellingen van Mumble.
-</translation>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -6592,10 +6544,6 @@ Valide opties zijn:
<translation>Verbind opnieuw met laatst bezochte server bij opstarten</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Plug-in/nieuwe overlay-updates downloaden bij starten</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privacy</translation>
</message>
@@ -6621,6 +6569,14 @@ Voorkomt dat Mumble potentieel identificerende informatie over het besturingssys
<source>Hide public server list</source>
<translation>Verberg publieke server-lijst</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7175,31 +7131,189 @@ Klik op de onderstaande knop om deze bestanden naar de laatste versie bij te wer
<translation>Naam</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Geactiveerd</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Activeren</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Plugins bijwerken overslaan in foutopsporende-modus.</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Nieuwe of bijgewerkte plug-in naar %1 gedownload.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Nieuwe plug-in naar %1 installeren mislukt.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 verloor verbinding.</translation>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
</message>
+</context>
+<context>
+ <name>PluginUpdater</name>
<message>
- <source>%1 linked.</source>
- <translation>%1 verbonden.</translation>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_no.ts b/src/mumble/mumble_no.ts
index 62db26e34..07486d581 100644
--- a/src/mumble/mumble_no.ts
+++ b/src/mumble/mumble_no.ts
@@ -3850,6 +3850,10 @@ Dette feltet beskriver størrelsen på en LCD-enhet. Enten gitt i piksler (for g
<source>User stopped listening to channel</source>
<translation>Bruker stoppet å lytte til kanalen</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6204,12 +6208,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6529,10 +6534,6 @@ Valid options are:
<translation>Koble til tjeneren som sist ble brukt ved oppstart</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Last ned programtillegg og forgrunnsinformasjonsoppdateringer ved oppstart</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Personvern</translation>
</message>
@@ -6558,6 +6559,14 @@ Forhindrer klienten fra å sende potensielt identifiserende informasjon om opera
<source>Hide public server list</source>
<translation>Skjul offentlig tjenerliste</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7112,31 +7121,189 @@ Trykk på knappen nedefor for å oppgradere.</translation>
<translation>Navn</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Påskrudd</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Skru på</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Hopper over programoppdatering i feilrettingsmodus.</translation>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Programtillegg lastet ned eller oppdatert til %1.</translation>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Klarte ikke å installere nytt programtillegg til %1.</translation>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 mistet lenke.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 er lenket.</translation>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_oc.ts b/src/mumble/mumble_oc.ts
index 34f7cec51..c5b0ce572 100644
--- a/src/mumble/mumble_oc.ts
+++ b/src/mumble/mumble_oc.ts
@@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6122,12 +6126,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6447,10 +6452,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Nom</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_pl.ts b/src/mumble/mumble_pl.ts
index 65267a939..05d9c5b9b 100644
--- a/src/mumble/mumble_pl.ts
+++ b/src/mumble/mumble_pl.ts
@@ -3839,6 +3839,10 @@ Te pole opisuje rozmiar urządzenia LCD. Rozmiar jest podany w pikselach lub w z
<source>User stopped listening to channel</source>
<translation>Użytkownik przestał słuchać kanału</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6215,12 +6219,13 @@ toggledeaf
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6270,61 +6275,7 @@ Valid options are:
Otherwise the locale will be permanently saved to
Mumble&apos;s settings.
</source>
- <translation>Użycie: mumble [opcje] [&lt;url&gt;]
-
-&lt;url&gt; określa adres URL, z którym należy się połączyć po uruchomieniu zamiast pokazywać
-okno połączenia, ma następującą postać:
-mumble://[&lt;nazwa użytkownika&gt;[:&lt;hasło&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;kanał&gt;[/&lt;podkanał&gt;...]][?wersja=&lt;x.y.z&gt;]
-
-Aby wywołać poprawną wersję klienta, należy ustawić parametr
-zapytania o wersję. Obecnie domyślnie jest to 1.2.0.
-
-Prawidłowe opcje to:
- -h, --help Pokaż ten tekst pomocy i zakończ.
- -m, --multiple
- Zezwalaj na uruchamianie wielu instancji klienta.
- -c, --config
- Określ alternatywny plik konfiguracyjny.
- Jeśli używasz tego do uruchamiania wielu wystąpień Mumble jednocześnie,
- upewnij się, że ustawiono alternatywną wartość „bazy danych” w konfiguracji.
- -n, --noidentity
- Blokuj ładowanie plików tożsamości (tj. certyfikatów).
- -jn, --jackname &lt;arg&gt;
- Ustaw niestandardową nazwę klienta Jack.
- --license
- Pokaż licencję Mumble.
- --authors
- Pokaż autorów Mumble.
- --third-party-licenses
- Pokaż licencje na oprogramowanie innych firm używane przez Mumble.
- --window-title-ext &lt;arg&gt;
- Ustawia niestandardowe rozszerzenie tytułu okna.
- --dump-input-stream
- Zrzuca strumienie PCM w różnych częściach łańcucha wejściowego
- (przydatne do debugowania)
- - surowe wejście mikrofonowe
- - odczyt głośnika w celu usunięcia echa
- - przetworzone wejście mikrofonowe
- --print-echocancel-queue
- Wyświetl na stdout stan kolejki anulowania echa
- (przydatne do debugowania)
- --translation-dir &lt;dir&gt;
- Określa dodatkowe tłumaczenie dla &lt;kat&gt;, w którym
- Mumble wyszuka pliki tłumaczeń, które nadpiszą
- dołączone
- Katalogi dodane w ten sposób mają wyższy priorytet niż
- domyślne lokalizacje używane w inny sposób
- --print-translation-dirs
- Wyświetl ścieżki, których będzie szukał Mumble
- plików tłumaczeń, które zastępują dołączone pliki.
- (Przydatne dla tłumaczy testujących swoje tłumaczenia)
- --locale &lt;ust. reg,&gt;
- Nadpisz ustawienia regionalne w ustawieniach Mumble za pomocą
- ustawień reg., które odpowiada podanemu łańcuchowi ustawień reg.
- Jeśli format jest nieprawidłowy, Mumble wyświetli błąd.
- W przeciwnym razie ustawienia regionalne zostaną trwale zapisane w
- ustawieniach Mumble.
-</translation>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -6594,10 +6545,6 @@ Prawidłowe opcje to:
<translation>Przy starcie połącz ponownie do ostatniego serwera</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Pobieraj aktualizacje nakładki oraz wtyczek przy starcie</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Prywatność</translation>
</message>
@@ -6623,6 +6570,14 @@ Uniemożliwia klientowi wysyłanie potencjalnie identyfikujących informacji o s
<source>Hide public server list</source>
<translation>Ukryj listę serwerów publicznych</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7177,31 +7132,189 @@ Aby uaktualnić pliki do najnowszych wersji, kliknij przycisk poniżej.</transla
<translation>Nazwa</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Włączona</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Włącz</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Pomijanie aktualizacji wtyczek w trybie debugowania.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Pobrano nowy lub uaktualniony plugin do %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Nie udało się zainstalować nowego pluginu do %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 utracił połączenie.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 połączony.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_pt_BR.ts b/src/mumble/mumble_pt_BR.ts
index 451b875f6..e5e34bf6f 100644
--- a/src/mumble/mumble_pt_BR.ts
+++ b/src/mumble/mumble_pt_BR.ts
@@ -3838,6 +3838,10 @@ Este campo descreve o tamanho de um dispositivo LCD. O tamanho é dado em pixels
<source>User stopped listening to channel</source>
<translation>Usuário parou de ouvir o canal</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6192,12 +6196,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6517,10 +6522,6 @@ Valid options are:
<translation>Reconectar ao último servidor ao iniciar</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Baixar atualizações de complementos e sobreimpressão ao iniciar</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Privacidade</translation>
</message>
@@ -6546,6 +6547,14 @@ Evita que o cliente envie informações potencialmente capazes de identificaçã
<source>Hide public server list</source>
<translation>Ocultar lista de servidores públicos</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7100,31 +7109,189 @@ Para atualizar estes arquivos para suas últimas versões, clique no botão abai
<translation>Nome</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Ativo</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Ativar</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Pulando atualização de complementos no modo de depuração.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Baixou complemento novo ou atualizado para %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Falha ao instalar a novo complemento para %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 perdeu vínculo.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 vinculado.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_pt_PT.ts b/src/mumble/mumble_pt_PT.ts
index dfa50ea20..f683c817d 100644
--- a/src/mumble/mumble_pt_PT.ts
+++ b/src/mumble/mumble_pt_PT.ts
@@ -3824,6 +3824,10 @@ Este campo descreve o tamanho de um dispositivo LCD. O tamanho é dado em pixels
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6175,12 +6179,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6500,10 +6505,6 @@ Valid options are:
<translation>Ligar novamente ao último servidor ao iniciar</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Tranferir atualizações de plugins e sobreposição ao iniciar</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6528,6 +6529,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7082,31 +7091,189 @@ Para atualizar estes ficheiros para suas últimas versões, clique no botão aba
<translation>Nome</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Ativo</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Saltar atualização de plugin no modo de depuração.</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Transferido plugin novo ou atualizado para %1.</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Falha ao instalar a novo plugin para %1.</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 perdeu ligação.</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 ligado.</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_ro.ts b/src/mumble/mumble_ro.ts
index ce4203450..be4b70950 100644
--- a/src/mumble/mumble_ro.ts
+++ b/src/mumble/mumble_ro.ts
@@ -3783,6 +3783,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6126,12 +6130,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6451,10 +6456,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6479,6 +6480,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7029,30 +7038,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Nume</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_ru.ts b/src/mumble/mumble_ru.ts
index a1957a4a2..007d7a382 100644
--- a/src/mumble/mumble_ru.ts
+++ b/src/mumble/mumble_ru.ts
@@ -3792,6 +3792,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation> Пользователь перестал слушать канал</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6168,12 +6172,13 @@ Valid actions are:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6493,10 +6498,6 @@ Valid options are:
<translation>Подключаться к последнему серверу при запуске</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Загружать обновления плагинов и табло при запуске</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Конфиденциальность</translation>
</message>
@@ -6521,6 +6522,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation>Скрыть список публичных серверов</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7075,31 +7084,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>Имя</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Включено</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Включить</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Пропустить обновление плагина в режиме отладки.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Загрузка нового или обновленного плагина в %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Ошибка при установке нового плагина в %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 потерял связь.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 подключен.</translation>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_si.ts b/src/mumble/mumble_si.ts
index 21842df7b..967c0a939 100644
--- a/src/mumble/mumble_si.ts
+++ b/src/mumble/mumble_si.ts
@@ -3753,6 +3753,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>%1 link</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6084,12 +6088,13 @@ Otherwise abort and check your certificate and username.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6409,10 +6414,6 @@ Prevents the client from sending potentially identifying information about the o
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Submit anonymous statistics to the Mumble project</source>
<translation type="unfinished"></translation>
</message>
@@ -6436,6 +6437,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Network</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -6939,10 +6948,6 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished"></translation>
</message>
<message>
- <source>Enabled</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Reloads all plugins</source>
<translation type="unfinished"></translation>
</message>
@@ -6986,27 +6991,189 @@ To upgrade these files to their latest versions, click the button below.</source
<source>Plugin has no about function.</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>%1 lost link.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_sv.ts b/src/mumble/mumble_sv.ts
index d6df92fe9..3f4d1708c 100644
--- a/src/mumble/mumble_sv.ts
+++ b/src/mumble/mumble_sv.ts
@@ -3838,6 +3838,10 @@ Detta fält beskriver storleken av en LCD-enhet. Storleken mäts i pixlar (för
<source>User stopped listening to channel</source>
<translation>Anändare slutade lyssna på kanalen</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6214,12 +6218,13 @@ Giltiga åtgärder är:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6269,61 +6274,7 @@ Valid options are:
Otherwise the locale will be permanently saved to
Mumble&apos;s settings.
</source>
- <translation>Användning: mumble [options] [&lt;url&gt;]
-
-&lt;url&gt; anger en URL som ska anslutas till efter uppstart istället för att visa
-anslutningsfönstret, och har följande form:
-mumble://[&lt;användarnamn&gt;[:&lt;lösenord&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;kanal&gt;[/&lt;underkanal&gt;...]][?version=&lt;x.y.z&gt;]
-
-Förfrågningsparametern version måste ställas in för att kunna åberopa
-rätt klientversion. För närvarande är standardvärdet 1.2.0.
-
-Giltiga alternativ är:
- -h, --help Visar denna hjälptext.
- -m, --multiple
- Tillåt att flera instanser av klienten startas.
- -c, --config
- Ange en alternativ konfigurationsfil.
- Om du använder detta för att köra flera instanser av Mumble på en gång,
- se till att ställa in ett alternativt &quot;databas&quot; -värde i konfigurationen.
- -n, --noidentity
- Dämpa inläsning av identitetsfiler (dvs. certifikat.)
- -jn, --jackname &lt;arg&gt;
- Ange anpassat Jack-klientnamn.
- --license
- Visa Mumble-licensen.
- --authors
- VIsa Mumble-skapare.
- --third-party-licenses
- Visa licenser för tredje-partmjukvara som används av Mumble.
- --window-title-ext &lt;arg&gt;
- Ställer in ett anpassat fönstertiteltillägg.
- --dump-input-streams
- Dumpa PCM-strömmar vid olika delar av ingångskedjan
- (användbart för felsökningsändamål)
- - rå mikrofoningång
- - högtalaravläsning för ekodämpning
- - bearbetad mikrofoningång
- --print-echocancel-queue
- Skriv ut på stdout ekoställningskön
- (användbart för felsökningsändamål)
---translation-dir &lt;dir&gt;
- Anger en ytterligare översättning för &lt;dir&gt; där
- Mumble söker efter översättningsfiler som skrivs över
- de medföljande
- kataloger som läggs till på detta sätt har högre prioritet än
- standardplatserna som används annars
---print-translation-dirs
- Skriv ut de vägar som Mumble söker efter
- översättningsfiler som skriver över de medföljande filerna.
- (Användbart för översättare som testar sina översättningar)
- --locale &lt;locale&gt;
- Skriver över lokaladressen i Mumbles inställningar med en
- lokal som motsvarar den angivna lokalsträngen.
- Om formatet är ogiltigt kommer Mumble att göra ett fel.
- Annars sparas språket permanent i
- Mumbles inställningar.
-</translation>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -6593,10 +6544,6 @@ Giltiga alternativ är:
<translation>Återanslut till den senaste servern vid uppstart</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Ladda ner uppdateringar för insticksmoduler och överlag vid uppstart</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Sekretess</translation>
</message>
@@ -6622,6 +6569,14 @@ Förhindrar klienten från att skicka potentiellt identifierande information om
<source>Hide public server list</source>
<translation>Dölj offentlig serverlista</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7176,31 +7131,189 @@ Tryck på knappen nedan för att uppgradera dessa filer till de senaste versione
<translation>Namn</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Aktiverad</translation>
+ <source>Enable</source>
+ <translation type="unfinished">Aktivera</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Hoppar över uppdatering för insticksmoduler i felsökningsläge.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Laddade ner ny/uppdaterad insticksmodul till %1.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Kude inte installera ny insticksmodul till %1.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 förlorade länk.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 länkad.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_te.ts b/src/mumble/mumble_te.ts
index 5615b89c2..3c894a928 100644
--- a/src/mumble/mumble_te.ts
+++ b/src/mumble/mumble_te.ts
@@ -3790,6 +3790,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6133,12 +6137,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6464,10 +6469,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6492,6 +6493,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7042,30 +7051,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished"> నామము</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_th.ts b/src/mumble/mumble_th.ts
index c85c22f6d..bcff56ff3 100644
--- a/src/mumble/mumble_th.ts
+++ b/src/mumble/mumble_th.ts
@@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6120,12 +6124,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6445,10 +6450,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">ชื่อ</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_tr.ts b/src/mumble/mumble_tr.ts
index 8c44ac139..524a04bf1 100644
--- a/src/mumble/mumble_tr.ts
+++ b/src/mumble/mumble_tr.ts
@@ -3836,6 +3836,10 @@ Bu alan LCD aygıtın boyutunu belirtir. Boyut ya piksel olarak (Grafik LCD ekra
<source>User stopped listening to channel</source>
<translation>Kullanıcı kanalı dinlemeye son verdi</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6188,12 +6192,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6513,10 +6518,6 @@ Valid options are:
<translation>Başladığında son sunucuya bağlan</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>Başladığında eklenti ve yerpaylaşan güncellemelerini indir</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>Gizlilik</translation>
</message>
@@ -6542,6 +6543,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7096,31 +7105,189 @@ Bu dosyaları son sürümlerine güncellemek için aşağıdaki düğmeyi tıkla
<translation>İsim</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>Etkin</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>Hata ayıklama kipinde eklenti güncellemesi atlanıyor.</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>Yeni ya da güncelleştirilmiş eklenti %1 konumuna indirildi.</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>Yeni eklenti %1 konumuna kurulamadı.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 bağlantı kaybetti.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 bağlandı.</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_uk.ts b/src/mumble/mumble_uk.ts
index c58ae0fd3..3b075ef7e 100644
--- a/src/mumble/mumble_uk.ts
+++ b/src/mumble/mumble_uk.ts
@@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6122,12 +6126,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6447,10 +6452,6 @@ Valid options are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.</source
<translation type="unfinished">Ім&apos;я</translation>
</message>
<message>
- <source>Enabled</source>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
<message>
- <source>Skipping plugin update in debug mode.</source>
+ <source>Do you want to update the selected plugins?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/mumble/mumble_zh_CN.ts b/src/mumble/mumble_zh_CN.ts
index d3527a07f..df68662bb 100644
--- a/src/mumble/mumble_zh_CN.ts
+++ b/src/mumble/mumble_zh_CN.ts
@@ -3837,6 +3837,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation>用户停止监听频道</translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6212,12 +6216,13 @@ Valid actions are:
</translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6267,54 +6272,7 @@ Valid options are:
Otherwise the locale will be permanently saved to
Mumble&apos;s settings.
</source>
- <translation>用法:mumble [选项] [&lt;URL&gt;]
-
-&lt;URL&gt; 指定启动时连接的 URL,而不是显示连接窗口,URL 的格式为:
-mumble://[&lt;用户名&gt;[:&lt;密码&gt;]@]&lt;主机名&gt;[:&lt;端口&gt;][/&lt;频道名&gt;[/&lt;子频道名&gt;...]][?version=&lt;x.y.z&gt;]
-
-必须设置 version 请求参数以调用正确的客户端版本,当前的默认值为 1.2.0。
-
-可用的选项:
- -h, --help 显示此帮助信息并退出。
- -m, --multiple
- 允许启动多个客户端实例。
- -c, --config
- 指定替代配置文件。
- 如果您使用此参数同时运行多个 Mumble 实例,
- 请确保在配置文件内设置替代 &apos;database&apos; 选项。
- -n, --noidentity
- 禁止加载身份认证文件(即证书)。
- -jn, --jackname &lt;参数&gt;
- 设置自定义 Jack 客户端名称。
- --license
- 显示 Mumble 许可。
- --authors
- 显示 Mumble 作者。
- --third-party-licenses
- 显示 Mumble 使用的第三方软件的许可。
- --window-title-ext &lt;参数&gt;
- 设置自定义窗口标题后缀名。
- --dump-input-streams
- 转储输入链上各部分的 PCM 流。
- (适用于调试目的)
- - 原始麦克风输入
- - 扬声器回声消除重读取
- - 已处理麦克风输入
- --print-echocancel-queue
- 向标准输出打印回声消除队列状态。
- (适用于调试目的)
- --translation-dir &lt;目录&gt;
- 指定一个额外的目录,Mumble 会在其中搜索翻译文件
- 来覆盖内置的翻译。通过此方式添加的目录比其它情况
- 下的默认位置具有更高的优先级。
- --print-translation-dirs
- 输出 Mumble 会在哪些目录搜索翻译文件以覆盖内置翻译。
- (适用于译者测试自己的翻译)
- --locale &lt;区域语言代码&gt;
- 用指定字符串对应的语言覆盖 Mumble 的语言设置。
- 如果字符串格式无效,Mumble 会出错。
- 否则,指定的语言会永久保存到 Mumble 设置中。
-</translation>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -6584,10 +6542,6 @@ mumble://[&lt;用户名&gt;[:&lt;密码&gt;]@]&lt;主机名&gt;[:&lt;端口&gt;]
<translation>启动时自动连接上次的服务器</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>启动时下载插件和游戏内界面更新</translation>
- </message>
- <message>
<source>Privacy</source>
<translation>隐私</translation>
</message>
@@ -6613,6 +6567,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation>隐藏公共服务器列表</translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7167,31 +7129,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>名称</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>启用</translation>
+ <source>Enable</source>
+ <translation type="unfinished">启用</translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginInstaller</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>在调试模式跳过插件更新。</translation>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>下载新版或升级插件到 %1。</translation>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>安装新插件到 %1 失败。</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 链接丢失。</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 已链接。</translation>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginManager</name>
+ <message>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_zh_HK.ts b/src/mumble/mumble_zh_HK.ts
index 3f4d56f71..3cfe84124 100644
--- a/src/mumble/mumble_zh_HK.ts
+++ b/src/mumble/mumble_zh_HK.ts
@@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6125,12 +6129,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6450,10 +6455,6 @@ Valid options are:
<translation>啟動時自動連接到最後使用的伺服器</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>啟動時下載外掛與浮動視窗的更新</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7032,31 +7041,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>名稱</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>啟用</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>在除錯模式中忽略外掛更新訊息。</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>下載或更新 %1 外掛。</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>安裝 %1 外掛失敗。</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 失去關聯。</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 已關聯。</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/mumble/mumble_zh_TW.ts b/src/mumble/mumble_zh_TW.ts
index 2ff9d3f8b..979b4f3c8 100644
--- a/src/mumble/mumble_zh_TW.ts
+++ b/src/mumble/mumble_zh_TW.ts
@@ -3805,6 +3805,10 @@ This field describes the size of an LCD device. The size is given either in pixe
<source>User stopped listening to channel</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Plugin message</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>LogConfig</name>
@@ -6148,12 +6152,13 @@ Valid actions are:
<translation type="unfinished"></translation>
</message>
<message>
- <source>Usage: mumble [options] [&lt;url&gt;]
+ <source>Usage: mumble [options] [&lt;url&gt; | &lt;plugin_list&gt;]
&lt;url&gt; specifies a URL to connect to after startup instead of showing
the connection window, and has the following form:
mumble://[&lt;username&gt;[:&lt;password&gt;]@]&lt;host&gt;[:&lt;port&gt;][/&lt;channel&gt;[/&lt;subchannel&gt;...]][?version=&lt;x.y.z&gt;]
+&lt;plugin_list&gt; is a list of plugin files that shall be installed
The version query parameter has to be set in order to invoke the
correct client version. It currently defaults to 1.2.0.
@@ -6473,10 +6478,6 @@ Valid options are:
<translation>啟動時自動連接到最後使用的伺服器</translation>
</message>
<message>
- <source>Download plugin and overlay updates on startup</source>
- <translation>啟動時下載外掛與浮動視窗的更新</translation>
- </message>
- <message>
<source>Privacy</source>
<translation type="unfinished"></translation>
</message>
@@ -6501,6 +6502,14 @@ Prevents the client from sending potentially identifying information about the o
<source>Hide public server list</source>
<translation type="unfinished"></translation>
</message>
+ <message>
+ <source>Check for plugin updates on startup</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Automatically download and install plugin updates</source>
+ <translation type="unfinished"></translation>
+ </message>
</context>
<context>
<name>Overlay</name>
@@ -7054,31 +7063,189 @@ To upgrade these files to their latest versions, click the button below.</source
<translation>名稱</translation>
</message>
<message>
- <source>Enabled</source>
- <translation>已啟用</translation>
+ <source>Enable</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PA</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>KeyEvents</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install a plugin from a local file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Install plugin...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unload</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The plugin was installed successfully</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to deactivate all requested features for plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether the positional audio feature of this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>This plugin does not provide support for positional audio</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Whether this plugin should be enabled</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginInstaller</name>
+ <message>
+ <source>PluginInstaller</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You are about to install the plugin listed below. Do you wish to proceed?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;No</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>&amp;Yes</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The file &quot;%1&quot; is not a valid plugin file!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Found more than one plugin library for the current OS in &quot;%1&quot; (&quot;%2&quot; and &quot;%3&quot;)!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to find a plugin for the current OS in &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to load plugin &quot;%1&quot; - check the plugin interface!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to delete old plugin at &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to copy plugin library from &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to move plugin library to &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
- <name>Plugins</name>
+ <name>PluginManager</name>
<message>
- <source>Skipping plugin update in debug mode.</source>
- <translation>在除錯模式忽略外掛更新訊息。</translation>
+ <source>%1 lost link</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Downloaded new or updated plugin to %1.</source>
- <translation>下載或更新 %1 外掛。</translation>
+ <source>%1 linked</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>Failed to install new plugin to %1.</source>
- <translation>安裝 %1 外掛失敗。</translation>
+ <source>Plugin &quot;%1&quot; encountered a permanent error in positional data gathering</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 lost link.</source>
- <translation>%1 連接遺失。</translation>
+ <source>Non-plugin found in plugin directory: &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Failed at loading manual plugin: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PluginUpdater</name>
+ <message>
+ <source>PluginUpdater</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>The following plugins can be updated.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Select all</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Plugin</source>
+ <translation type="unfinished"></translation>
</message>
<message>
- <source>%1 linked.</source>
- <translation>%1 已連接。</translation>
+ <source>Download-URL</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Do you want to update the selected plugins?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (%3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Update for plugin &quot;%1&quot; failed due to too many redirects</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unable to download plugin update for &quot;%1&quot; from &quot;%2&quot; (HTTP status code %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Successfully updated plugin &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
</message>
</context>
<context>
diff --git a/src/murmur/Messages.cpp b/src/murmur/Messages.cpp
index cfde87e2a..6fb23e141 100644
--- a/src/murmur/Messages.cpp
+++ b/src/murmur/Messages.cpp
@@ -10,6 +10,7 @@
#include "Group.h"
#include "Message.h"
#include "Meta.h"
+#include "MumbleConstants.h"
#include "Server.h"
#include "ServerDB.h"
#include "ServerUser.h"
@@ -2165,6 +2166,61 @@ void Server::msgServerConfig(ServerUser *, MumbleProto::ServerConfig &) {
void Server::msgSuggestConfig(ServerUser *, MumbleProto::SuggestConfig &) {
}
+void Server::msgPluginDataTransmission(ServerUser *sender, MumbleProto::PluginDataTransmission &msg) {
+ // A client's plugin has sent us a message that we shall delegate to its receivers
+
+ if (sender->m_pluginMessageBucket.ratelimit(1)) {
+ qWarning("Dropping plugin message sent from \"%s\" (%d)", qUtf8Printable(sender->qsName), sender->uiSession);
+ return;
+ }
+
+ if (!msg.has_data() || !msg.has_dataid()) {
+ // Messages without data and/or without a data ID can't be used by the clients. Thus we don't even have to send them
+ return;
+ }
+
+ if (msg.data().size() > Mumble::Plugins::PluginMessage::MAX_DATA_LENGTH) {
+ qWarning("Dropping plugin message sent from \"%s\" (%d) - data too large", qUtf8Printable(sender->qsName), sender->uiSession);
+ return;
+ }
+ if (msg.dataid().size() > Mumble::Plugins::PluginMessage::MAX_DATA_ID_LENGTH) {
+ qWarning("Dropping plugin message sent from \"%s\" (%d) - data ID too long", qUtf8Printable(sender->qsName), sender->uiSession);
+ return;
+ }
+
+ // Always set the sender's session and don't rely on it being set correctly (would
+ // allow spoofing the sender's session)
+ msg.set_sendersession(sender->uiSession);
+
+ // Copy needed data from message in order to be able to remove info about receivers from the message as this doesn't
+ // matter for the client
+ size_t receiverAmount = msg.receiversessions_size();
+ const ::google::protobuf::RepeatedField< ::google::protobuf::uint32 > receiverSessions = msg.receiversessions();
+
+ msg.clear_receiversessions();
+
+ QSet<uint32_t> uniqueReceivers;
+ uniqueReceivers.reserve(receiverSessions.size());
+
+ for(int i = 0; static_cast<size_t>(i) < receiverAmount; i++) {
+ uint32_t userSession = receiverSessions.Get(i);
+
+ if (!uniqueReceivers.contains(userSession)) {
+ uniqueReceivers.insert(userSession);
+ } else {
+ // Duplicate entry -> ignore
+ continue;
+ }
+
+ ServerUser *receiver = qhUsers.value(receiverSessions.Get(i));
+
+ if (receiver) {
+ // We can simply redirect the message we have received to the clients
+ sendMessage(receiver, msg);
+ }
+ }
+}
+
#undef RATELIMIT
#undef MSG_SETUP
#undef MSG_SETUP_NO_UNIDLE
diff --git a/src/murmur/Meta.cpp b/src/murmur/Meta.cpp
index 6ebaeb18d..5e1456167 100644
--- a/src/murmur/Meta.cpp
+++ b/src/murmur/Meta.cpp
@@ -104,6 +104,9 @@ MetaParams::MetaParams() {
iMessageLimit = 1;
iMessageBurst = 5;
+ iPluginMessageLimit = 4;
+ iPluginMessageBurst = 15;
+
qsCiphers = MumbleSSL::defaultOpenSSLCipherString();
bLogGroupChanges = false;
@@ -402,6 +405,9 @@ void MetaParams::read(QString fname) {
iMessageLimit = typeCheckedFromSettings("messagelimit", 1);
iMessageBurst = typeCheckedFromSettings("messageburst", 5);
+ iPluginMessageLimit = typeCheckedFromSettings("pluginmessagelimit", 4);
+ iPluginMessageBurst = typeCheckedFromSettings("pluginmessageburst", 15);
+
bool bObfuscate = typeCheckedFromSettings("obfuscate", false);
if (bObfuscate) {
qWarning("IP address obfuscation enabled.");
diff --git a/src/murmur/Meta.h b/src/murmur/Meta.h
index bac90df5b..91b48f535 100644
--- a/src/murmur/Meta.h
+++ b/src/murmur/Meta.h
@@ -104,6 +104,9 @@ public:
unsigned int iMessageLimit;
unsigned int iMessageBurst;
+ unsigned int iPluginMessageLimit;
+ unsigned int iPluginMessageBurst;
+
QSslCertificate qscCert;
QSslKey qskKey;
diff --git a/src/murmur/Server.cpp b/src/murmur/Server.cpp
index 4c8dd0939..c0c979814 100644
--- a/src/murmur/Server.cpp
+++ b/src/murmur/Server.cpp
@@ -418,6 +418,8 @@ void Server::readParams() {
qrChannelName = Meta::mp.qrChannelName;
iMessageLimit = Meta::mp.iMessageLimit;
iMessageBurst = Meta::mp.iMessageBurst;
+ iPluginMessageLimit = Meta::mp.iPluginMessageLimit;
+ iPluginMessageBurst = Meta::mp.iPluginMessageBurst;
qvSuggestVersion = Meta::mp.qvSuggestVersion;
qvSuggestPositional = Meta::mp.qvSuggestPositional;
qvSuggestPushToTalk = Meta::mp.qvSuggestPushToTalk;
@@ -526,6 +528,15 @@ void Server::readParams() {
if (iMessageBurst < 1) { // Prevent disabling messages entirely
iMessageBurst = 1;
}
+
+ iPluginMessageLimit = getConf("mpluginessagelimit", iPluginMessageLimit).toUInt();
+ if (iPluginMessageLimit < 1) { // Prevent disabling messages entirely
+ iPluginMessageLimit = 1;
+ }
+ iPluginMessageBurst = getConf("pluginmessageburst", iPluginMessageBurst).toUInt();
+ if (iPluginMessageBurst < 1) { // Prevent disabling messages entirely
+ iPluginMessageBurst = 1;
+ }
}
void Server::setLiveConf(const QString &key, const QString &value) {
diff --git a/src/murmur/Server.h b/src/murmur/Server.h
index abebd0ffe..ac854cea6 100644
--- a/src/murmur/Server.h
+++ b/src/murmur/Server.h
@@ -140,6 +140,9 @@ public:
unsigned int iMessageLimit;
unsigned int iMessageBurst;
+ unsigned int iPluginMessageLimit;
+ unsigned int iPluginMessageBurst;
+
QVariant qvSuggestVersion;
QVariant qvSuggestPositional;
QVariant qvSuggestPushToTalk;
diff --git a/src/murmur/ServerUser.cpp b/src/murmur/ServerUser.cpp
index f2dac8abb..367889bee 100644
--- a/src/murmur/ServerUser.cpp
+++ b/src/murmur/ServerUser.cpp
@@ -13,7 +13,8 @@
#endif
ServerUser::ServerUser(Server *p, QSslSocket *socket)
- : Connection(p, socket), User(), s(nullptr), leakyBucket(p->iMessageLimit, p->iMessageBurst) {
+ : Connection(p, socket), User(), s(nullptr),
+ leakyBucket(p->iMessageLimit, p->iMessageBurst), m_pluginMessageBucket(5, 20) {
sState = ServerUser::Connected;
sUdpSocket = INVALID_SOCKET;
diff --git a/src/murmur/ServerUser.h b/src/murmur/ServerUser.h
index fd40071ca..c34ebeaf7 100644
--- a/src/murmur/ServerUser.h
+++ b/src/murmur/ServerUser.h
@@ -147,6 +147,7 @@ public:
QMap< QString, QString > qmWhisperRedirect;
LeakyBucket leakyBucket;
+ LeakyBucket m_pluginMessageBucket;
int iLastPermissionCheck;
QMap< int, unsigned int > qmPermissionSent;