From 8007c9c0d5fa4501a6195854450eb6347bf8a253 Mon Sep 17 00:00:00 2001 From: Dillon Skaggs Date: Sun, 5 Jan 2025 02:58:30 -0600 Subject: [PATCH] tweak(mumble): simplify how we send clients and channels for the VoiceTarget packet The previous implementation allowed duplicates, and was more complex than what we need since we don't care about channel linking (we would always set it to false) This will also reset connections if they have too many in flight TCP pings This removes all the swaps from std::wstring <-> std::string Send Auth packet immediately after Version In the spec we have to send the Auth packet after Version, we opted to do it whenever we get the version packet back, this isn't really needed and slows down the exchange, since the server will only send `CryptoSetup` after we send this packet. --- .../extra-natives-five/src/NuiAudioSink.cpp | 18 +- .../gta-net-five/src/MumbleVoice.cpp | 487 ++++++++++-------- .../voip-mumble/include/MumbleAudioSink.h | 2 +- .../voip-mumble/include/MumbleClient.h | 22 +- .../voip-mumble/include/MumbleClientImpl.h | 4 +- .../voip-mumble/include/MumbleClientState.h | 20 +- .../voip-mumble/src/MumbleAudioOutput.cpp | 8 +- .../voip-mumble/src/MumbleClient.cpp | 106 ++-- .../src/MumbleClientState_Channel.cpp | 4 +- .../src/MumbleClientState_User.cpp | 11 +- .../voip-mumble/src/MumbleVersionHandler.cpp | 10 - ext/native-decls/MumbleSetServerAddress.md | 2 + 12 files changed, 358 insertions(+), 336 deletions(-) diff --git a/code/components/extra-natives-five/src/NuiAudioSink.cpp b/code/components/extra-natives-five/src/NuiAudioSink.cpp index 22c7da972d..2217a8e3af 100644 --- a/code/components/extra-natives-five/src/NuiAudioSink.cpp +++ b/code/components/extra-natives-five/src/NuiAudioSink.cpp @@ -1307,7 +1307,7 @@ extern "C" class MumbleAudioEntityBase { public: - MumbleAudioEntityBase(const std::wstring& name) + MumbleAudioEntityBase(const std::string& name) : m_position(rage::Vec3V{ 0.f, 0.f, 0.f }), m_positionForce(rage::Vec3V{ 0.f, 0.f, 0.f }), m_buffer(nullptr), @@ -1389,7 +1389,7 @@ class MumbleAudioEntityBase CPed* m_ped; - std::wstring m_name; + std::string m_name; std::function m_poller; }; @@ -1398,7 +1398,7 @@ template class MumbleAudioEntity : public rage::audEntity, public MumbleAudioEntityBase, public std::enable_shared_from_this> { public: - MumbleAudioEntity(const std::wstring& name) + MumbleAudioEntity(const std::string& name) : MumbleAudioEntityBase(name) { } @@ -1821,7 +1821,7 @@ class MumbleAudioSink : public IMumbleAudioSink public: void Process(); - MumbleAudioSink(const std::wstring& name); + MumbleAudioSink(const std::string& name); virtual ~MumbleAudioSink() override; virtual void SetPollHandler(const std::function& poller) override; @@ -1833,7 +1833,7 @@ class MumbleAudioSink : public IMumbleAudioSink void Reset(); private: - std::wstring m_name; + std::string m_name; int m_serverId; /// @@ -1860,11 +1860,9 @@ static std::set g_sinks; static std::shared_mutex g_submixMutex; static std::map g_submixIds; -MumbleAudioSink::MumbleAudioSink(const std::wstring& name) - : m_serverId(-1), m_position(rage::Vec3V{ 0.f, 0.f, 0.f }), m_distance(5.0f), m_overrideVolume(-1.0f), m_name(name) +MumbleAudioSink::MumbleAudioSink(const std::string& userName) + : m_serverId(-1), m_position(rage::Vec3V{ 0.f, 0.f, 0.f }), m_distance(5.0f), m_overrideVolume(-1.0f), m_name(userName) { - auto userName = ToNarrow(name); - if (userName.length() >= 2) { int serverId = atoi(userName.substr(1, userName.length() - 1).c_str()); @@ -2888,7 +2886,7 @@ static InitFunction initFunction([]() ProcessAudioSinks(); }); - OnGetMumbleAudioSink.Connect([](const std::wstring& name, fwRefContainer* sink) + OnGetMumbleAudioSink.Connect([](const std::string& name, fwRefContainer* sink) { fwRefContainer ref = new MumbleAudioSink(name); *sink = ref; diff --git a/code/components/gta-net-five/src/MumbleVoice.cpp b/code/components/gta-net-five/src/MumbleVoice.cpp index 1773cc998d..cd05486de4 100644 --- a/code/components/gta-net-five/src/MumbleVoice.cpp +++ b/code/components/gta-net-five/src/MumbleVoice.cpp @@ -33,6 +33,8 @@ #include #include +#include "ScriptWarnings.h" + #if __has_include() #include #endif @@ -358,6 +360,16 @@ static float* g_actorPos; #pragma comment(lib, "dsound.lib") +bool IsMumbleConnected() +{ + if (!g_mumble.connectionInfo) + { + return false; + } + + return g_mumble.connected && g_mumble.connectionInfo->isConnected; +} + static void Mumble_RunFrame() { if (!Instance::Get()->HasVariable("gameSettled")) @@ -370,20 +382,15 @@ static void Mumble_RunFrame() return; } - if (!g_mumble.connected || (g_mumble.connectionInfo && !g_mumble.connectionInfo->isConnected)) + if (!IsMumbleConnected()) { if (Mumble_ShouldConnect() && !g_mumble.connecting && !g_mumble.errored) { if (GetTickCount64() > g_mumble.nextConnectAt) { Mumble_Connect(); - - g_mumble.nextConnectDelay *= 2; - - if (g_mumble.nextConnectDelay > 30 * 1000) - { - g_mumble.nextConnectDelay = 30 * 1000; - } + + g_mumble.nextConnectDelay = std::min(g_mumble.nextConnectDelay * 2, 30'000); g_mumble.nextConnectAt = GetTickCount64() + g_mumble.nextConnectDelay; } @@ -760,7 +767,7 @@ static bool(*g_origGetPlayerHasHeadset)(void*, void*); static bool _getPlayerHasHeadset(void* mgr, void* plr) { - if (g_mumble.connected) + if (IsMumbleConnected()) { return true; } @@ -772,7 +779,7 @@ static float(*g_origGetLocalAudioLevel)(void* mgr, int localIdx); static float _getLocalAudioLevel(void* mgr, int localIdx) { - float mumbleLevel = (g_mumble.connected) ? g_mumbleClient->GetInputAudioLevel() : 0.0f; + float mumbleLevel = (IsMumbleConnected()) ? g_mumbleClient->GetInputAudioLevel() : 0.0f; return std::max(g_origGetLocalAudioLevel(mgr, localIdx), mumbleLevel); } @@ -787,7 +794,7 @@ static void _filterVoiceChatConfig(void* engine, char* config) #endif // disable voice if mumble is used - if (g_mumble.connected) + if (IsMumbleConnected()) { *config = 0; } @@ -800,35 +807,81 @@ static void _filterVoiceChatConfig(void* engine, char* config) static fx::TNativeHandler getPlayerName; static fx::TNativeHandler getServerId; +static std::optional getMumbleName(int playerId) +{ + int serverId = FxNativeInvoke::Invoke(getServerId, playerId); + + // if the server id is 0 then we don't have a player. + if (serverId == 0) + { + return std::nullopt; + } + + return fmt::sprintf("[%d] %s", + serverId, + FxNativeInvoke::Invoke(getPlayerName, playerId)); +} + static std::shared_ptr getAudioContext(int playerId) { - if (!g_mumble.connected) + const auto name = getMumbleName(playerId); + + // if the server id is 0 then we don't have a player. + if (!IsMumbleConnected() || !name) { return {}; } - std::string name = fmt::sprintf("[%d] %s", - FxNativeInvoke::Invoke(getServerId, playerId), - FxNativeInvoke::Invoke(getPlayerName, playerId)); - return g_mumbleClient->GetAudioContext(name); + return g_mumbleClient->GetAudioContext(*name); } static std::shared_ptr getAudioContextByServerId(int serverId) { - if (!g_mumble.connected) + if (!IsMumbleConnected()) { return {}; } - - std::string name = ToNarrow(g_mumbleClient->GetPlayerNameFromServerId(serverId)); + + std::string name = g_mumbleClient->GetPlayerNameFromServerId(serverId); + if (name.empty()) + { + return {}; + } + return g_mumbleClient->GetAudioContext(name); } -std::wstring getMumbleName(int playerId) +std::string GetMumbleChannel(int channelId) +{ + return fmt::sprintf("Game Channel %d", channelId); +} + +// Returns true if the voice target id valid to use with the `VoiceTarget` packet (1..30) +// see: https://github.com/citizenfx/fivem/blob/0ec3c8f9f6e715e65beca971712384d0300a553a/code/components/voip-server-mumble/src/Mumble.proto#L438-L441 +bool IsVoiceTargetIdValid(int id) { - return ToWide(fmt::sprintf("[%d] %s", - FxNativeInvoke::Invoke(getServerId, playerId), - FxNativeInvoke::Invoke(getPlayerName, playerId))); + return id >= 1 && id <= 30; +} + +// Ensures that mumble is connected before calling any mumble related functions +template +inline auto MakeMumbleNative(MumbleFn fn, uintptr_t defaultValue = 0) +{ + return [=](fx::ScriptContext& context) + { + if (!IsMumbleConnected()) + { + context.SetResult(defaultValue); + return; + } + + fn(context); + }; +}; + +static void InvalidTargetIdWarning(const std::string_view& nativeName) +{ + fx::scripting::Warningf("mumble", "%s: Tried to use an invalid targetId, the minimum target id is 1, the maximum is 30.", nativeName); } #include @@ -887,7 +940,7 @@ static HookFunction hookFunction([]() func(); } - if (!g_mumble.connected) + if (!IsMumbleConnected()) { return; } @@ -923,22 +976,18 @@ static HookFunction hookFunction([]() } }); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_TALKER_PROXIMITY", [](fx::ScriptContext& context) + + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_TALKER_PROXIMITY", MakeMumbleNative([](fx::ScriptContext& context) { float proximity = context.GetArgument(0); - if (g_mumble.connected) - { - g_mumbleClient->SetAudioDistance(proximity); - } - }); + g_mumbleClient->SetAudioDistance(proximity); + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_GET_TALKER_PROXIMITY", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_GET_TALKER_PROXIMITY", MakeMumbleNative([](fx::ScriptContext& context) { - float proximity = (g_mumble.connected) ? g_mumbleClient->GetAudioDistance() : 0.0f; - - context.SetResult(proximity); - }); + context.SetResult(g_mumbleClient->GetAudioDistance()); + }, 0.0f)); fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_ACTIVE", [](fx::ScriptContext& context) { @@ -950,313 +999,315 @@ static HookFunction hookFunction([]() context.SetResult(g_voiceActiveByScript); }); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_IS_PLAYER_TALKING", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_IS_PLAYER_TALKING", MakeMumbleNative([](fx::ScriptContext& context) { int playerId = context.GetArgument(0); bool isTalking = false; - if (g_mumble.connected) + if (playerId >= 0 && playerId < g_talkers.size()) { - if (playerId >= 0 && playerId < g_talkers.size()) - { - isTalking = g_talkers.test(playerId); - } + isTalking = g_talkers.test(playerId); } context.SetResult(isTalking); - }); + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_VOLUME_OVERRIDE", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_VOLUME_OVERRIDE", MakeMumbleNative([](fx::ScriptContext& context) { int playerId = context.GetArgument(0); float volume = context.GetArgument(1); - if (g_mumble.connected) + if (auto name = getMumbleName(playerId)) { - std::wstring name = getMumbleName(playerId); - - g_mumbleClient->SetClientVolumeOverride(name, volume); + g_mumbleClient->SetClientVolumeOverride(*name, volume); } - }); + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_VOLUME_OVERRIDE_BY_SERVER_ID", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_VOLUME_OVERRIDE_BY_SERVER_ID", MakeMumbleNative([](fx::ScriptContext& context) { int serverId = context.GetArgument(0); float volume = context.GetArgument(1); - if (g_mumble.connected) - { - g_mumbleClient->SetClientVolumeOverrideByServerId(serverId, volume); - } - }); + g_mumbleClient->SetClientVolumeOverrideByServerId(serverId, volume); + })); static VoiceTargetConfig vtConfigs[31]; - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_CLEAR_VOICE_TARGET", [](fx::ScriptContext& context) + + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_CLEAR_VOICE_TARGET", MakeMumbleNative([](fx::ScriptContext& context) { auto id = context.GetArgument(0); - if (id >= 0 && id < 31) + if (IsVoiceTargetIdValid(id)) { - if (g_mumble.connected) - { - vtConfigs[id] = {}; - g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); - } + vtConfigs[id] = {}; + g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); } - }); + else + { + InvalidTargetIdWarning("MUMBLE_CLEAR_VOICE_TARGET"); + } + })); + - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_REMOVE_VOICE_TARGET_CHANNEL", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_REMOVE_VOICE_TARGET_CHANNEL", MakeMumbleNative([](fx::ScriptContext& context) { auto id = context.GetArgument(0); auto channel = context.GetArgument(1); - if (id >= 0 && id < 31) + if (IsVoiceTargetIdValid(id)) { - if (g_mumble.connected) - { - auto targetChannel = fmt::sprintf("Game Channel %d", channel); - auto& targets = vtConfigs[id].targets; - targets.remove_if([targetChannel](auto& target) - { - return target.channel == targetChannel; - }); + auto targetChannel = GetMumbleChannel(channel); + auto& targets = vtConfigs[id]; + // we only want to mark the voice target config as pending if we actually modified it + // `erase()` will return `0` if it didn't remove anything or `1` if it did + if (targets.channels.erase(targetChannel)) + { g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); } } - }); + else + { + InvalidTargetIdWarning("MUMBLE_REMOVE_VOICE_TARGET_CHANNEL"); + } + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_REMOVE_VOICE_TARGET_PLAYER", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_REMOVE_VOICE_TARGET_PLAYER", MakeMumbleNative([](fx::ScriptContext& context) { auto id = context.GetArgument(0); auto playerId = context.GetArgument(1); - if (id >= 0 && id < 31) + if (IsVoiceTargetIdValid(id)) { - if (g_mumble.connected) + if (auto targetName = getMumbleName(playerId)) { - std::wstring targetName = getMumbleName(playerId); + auto& targets = vtConfigs[id]; - auto& targets = vtConfigs[id].targets; - targets.remove_if([targetName](auto& target) + // we only want to mark the voice target config as pending if we actually modified it + // `erase()` will return `0` if it didn't remove anything or `1` if it did + if (targets.users.erase(*targetName)) { - return target.users.size() > 0 && std::find(target.users.begin(), target.users.end(), targetName) != target.users.end(); - }); - - g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); + g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); + } } } - }); + else + { + InvalidTargetIdWarning("MUMBLE_REMOVE_VOICE_TARGET_PLAYER"); + } + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_REMOVE_VOICE_TARGET_PLAYER_BY_SERVER_ID", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_REMOVE_VOICE_TARGET_PLAYER_BY_SERVER_ID", MakeMumbleNative([](fx::ScriptContext& context) { auto id = context.GetArgument(0); auto serverId = context.GetArgument(1); - if (id >= 0 && id < 31) + if (IsVoiceTargetIdValid(id)) { - if (g_mumble.connected) - { - VoiceTargetConfig::Target ch; - std::wstring targetName = g_mumbleClient->GetPlayerNameFromServerId(serverId); + std::string targetName = g_mumbleClient->GetPlayerNameFromServerId(serverId); - if (!targetName.empty()) - { - auto& targets = vtConfigs[id].targets; - targets.remove_if([targetName](auto& target) - { - return target.users.size() > 0 && std::find(target.users.begin(), target.users.end(), targetName) != target.users.end(); - }); - } + // if the player doesn't exist then we don't want to update targetting + if (targetName.empty()) + { + return; } + + auto& targets = vtConfigs[id]; - g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); + if (targets.users.erase(targetName)) + { + g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); + } } - }); + else + { + InvalidTargetIdWarning("MUMBLE_REMOVE_VOICE_TARGET_PLAYER_BY_SERVER_ID"); + } + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_CLEAR_VOICE_TARGET_CHANNELS", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_CLEAR_VOICE_TARGET_CHANNELS", MakeMumbleNative([](fx::ScriptContext& context) { auto id = context.GetArgument(0); - if (id >= 0 && id < 31) + if (IsVoiceTargetIdValid(id)) { - if (g_mumble.connected) - { - auto& targets = vtConfigs[id].targets; - targets.remove_if([](auto& target) - { - return !target.channel.empty(); - }); + auto& targets = vtConfigs[id]; + + targets.channels.clear(); - g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); - } + g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); } - }); + else + { + InvalidTargetIdWarning("MUMBLE_CLEAR_VOICE_TARGET_CHANNELS"); + } + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_CLEAR_VOICE_TARGET_PLAYERS", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_CLEAR_VOICE_TARGET_PLAYERS", MakeMumbleNative([](fx::ScriptContext& context) { auto id = context.GetArgument(0); - if (id >= 0 && id < 31) + if (IsVoiceTargetIdValid(id)) { - if (g_mumble.connected) - { - auto& targets = vtConfigs[id].targets; - targets.remove_if([](auto& target) - { - return target.users.size() > 0; - }); + auto& targets = vtConfigs[id]; - g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); - } + targets.users.clear(); + + g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); } - }); + else + { + InvalidTargetIdWarning("MUMBLE_CLEAR_VOICE_TARGET_PLAYERS"); + } + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_ADD_VOICE_CHANNEL_LISTEN", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_ADD_VOICE_CHANNEL_LISTEN", MakeMumbleNative([](fx::ScriptContext& context) { auto channel = context.GetArgument(0); - if (g_mumble.connected) + const std::string channelName = GetMumbleChannel(channel); + if (g_mumbleClient->DoesChannelExist(channelName)) { - g_mumbleClient->AddListenChannel(fmt::sprintf("Game Channel %d", channel)); + g_mumbleClient->AddListenChannel(channelName); } - }); + else + { + fx::scripting::Warningf("mumble", "MUMBLE_ADD_VOICE_CHANNEL_LISTEN: Tried to call native on a channel that didn't exist"); + } + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_REMOVE_VOICE_CHANNEL_LISTEN", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_REMOVE_VOICE_CHANNEL_LISTEN", MakeMumbleNative([](fx::ScriptContext& context) { auto channel = context.GetArgument(0); - if (g_mumble.connected) - { - g_mumbleClient->RemoveListenChannel(fmt::sprintf("Game Channel %d", channel)); - } - }); + g_mumbleClient->RemoveListenChannel(GetMumbleChannel(channel)); + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_ADD_VOICE_TARGET_CHANNEL", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_ADD_VOICE_TARGET_CHANNEL", MakeMumbleNative([](fx::ScriptContext& context) { auto id = context.GetArgument(0); auto channel = context.GetArgument(1); - if (id >= 0 && id < 31) + if (IsVoiceTargetIdValid(id)) { - if (g_mumble.connected) - { - VoiceTargetConfig::Target ch; - ch.channel = fmt::sprintf("Game Channel %d", channel); - - vtConfigs[id].targets.push_back(ch); - g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); - } - } - }); - - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_DOES_CHANNEL_EXIST", [](fx::ScriptContext& context) { - auto channel = context.GetArgument(0); - - if (g_mumble.connected) - { - auto channelName = fmt::sprintf("Game Channel %d", channel); - context.SetResult(g_mumbleClient->DoesChannelExist(channelName)); + auto& targets = vtConfigs[id]; + + targets.channels.emplace(GetMumbleChannel(channel)); + + g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); } else { - context.SetResult(false); + InvalidTargetIdWarning("MUMBLE_ADD_VOICE_TARGET_CHANNEL"); } - }); + })); + + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_DOES_CHANNEL_EXIST", MakeMumbleNative([](fx::ScriptContext& context) { + auto channel = context.GetArgument(0); + + context.SetResult(g_mumbleClient->DoesChannelExist(GetMumbleChannel(channel))); + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_ADD_VOICE_TARGET_PLAYER", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_ADD_VOICE_TARGET_PLAYER", MakeMumbleNative([](fx::ScriptContext& context) { auto id = context.GetArgument(0); auto playerId = context.GetArgument(1); - if (id >= 0 && id < 31) + if (IsVoiceTargetIdValid(id)) { - if (g_mumble.connected) + auto& targets = vtConfigs[id]; + if (auto name = getMumbleName(playerId)) { - VoiceTargetConfig::Target ch; - std::wstring name = getMumbleName(playerId); - - ch.users.push_back(name); - - vtConfigs[id].targets.push_back(ch); + targets.users.emplace(*name); g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); } } - }); + else + { + InvalidTargetIdWarning("MUMBLE_ADD_VOICE_TARGET_PLAYER"); + } + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_ADD_VOICE_TARGET_PLAYER_BY_SERVER_ID", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_ADD_VOICE_TARGET_PLAYER_BY_SERVER_ID", MakeMumbleNative([](fx::ScriptContext& context) { auto id = context.GetArgument(0); int serverId = context.GetArgument(1); - if (id >= 0 && id < 31) + if (IsVoiceTargetIdValid(id)) { - if (g_mumble.connected) - { - VoiceTargetConfig::Target ch; - std::wstring name = g_mumbleClient->GetPlayerNameFromServerId(serverId); + std::string name = g_mumbleClient->GetPlayerNameFromServerId(serverId); - if (!name.empty()) - { - ch.users.push_back(name); - - vtConfigs[id].targets.push_back(ch); - g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); - } + if (!name.empty()) + { + vtConfigs[id].users.emplace(name); + g_mumbleClient->UpdateVoiceTarget(id, vtConfigs[id]); } } - }); + else + { + InvalidTargetIdWarning("MUMBLE_ADD_VOICE_TARGET_PLAYER_BY_SERVER_ID"); + } + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_VOICE_TARGET", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_VOICE_TARGET", MakeMumbleNative([](fx::ScriptContext& context) { auto id = context.GetArgument(0); + // We can set our voice target to 0..31 here (and only here!) if (id >= 0 && id < 31) { - if (g_mumble.connected) - { - g_mumbleClient->SetVoiceTarget(id); - } + g_mumbleClient->SetVoiceTarget(id); } - }); + else + { + fx::scripting::Warningf("mumble", "Invalid voice target id %d", id); + } + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_GET_VOICE_CHANNEL_FROM_SERVER_ID", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_GET_VOICE_CHANNEL_FROM_SERVER_ID", MakeMumbleNative([](fx::ScriptContext& context) { int serverId = context.GetArgument(0); int channelId = -1; - if (g_mumble.connected) - { - auto channelName = g_mumbleClient->GetVoiceChannelFromServerId(serverId); + auto channelName = g_mumbleClient->GetVoiceChannelFromServerId(serverId); - if (!channelName.empty()) + if (!channelName.empty()) + { + if (channelName.find("Game Channel ") == 0) { - if (channelName.find("Game Channel ") == 0) - { - channelId = std::stoi(channelName.substr(13)); - } - else if (channelName == "Root") - { - channelId = 0; - } + channelId = std::stoi(channelName.substr(13)); + } + else if (channelName == "Root") + { + channelId = 0; } } context.SetResult(channelId); - }); + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_IS_CONNECTED", [](fx::ScriptContext& context) + // MakeMumbleNative will return false automatically if we're not connected. + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_IS_CONNECTED", MakeMumbleNative([](fx::ScriptContext& context) { - context.SetResult(g_mumble.connected ? true : false); - }); + context.SetResult(true); + })); fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_SERVER_ADDRESS", [](fx::ScriptContext& context) { auto address = context.GetArgument(0); int port = context.GetArgument(1); + + // If we set our address to an empty and our port is -1 we should reset our override + if (address == "" && port == -1) + { + g_mumble.overridePeer = {}; + Mumble_Disconnect(true); + return; + } - boost::optional overridePeer = net::PeerAddress::FromString(fmt::sprintf("%s:%d", address, port), port); + auto formattedAddress = fmt::sprintf("%s:%d", address, port); + boost::optional overridePeer = net::PeerAddress::FromString(formattedAddress, port); if (overridePeer) { @@ -1266,7 +1317,7 @@ static HookFunction hookFunction([]() } else { - throw std::exception("Couldn't resolve Mumble server address."); + throw std::exception(va("Couldn't resolve Mumble server address %s.", formattedAddress)); } }); @@ -1284,21 +1335,15 @@ static HookFunction hookFunction([]() g_mumbleClient->SetAudioOutputDistance(dist); }); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_CLEAR_VOICE_CHANNEL", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_CLEAR_VOICE_CHANNEL", MakeMumbleNative([](fx::ScriptContext& context) { - if (g_mumble.connected) - { - g_mumbleClient->SetChannel("Root"); - } - }); + g_mumbleClient->SetChannel("Root"); + })); - fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_VOICE_CHANNEL", [](fx::ScriptContext& context) + fx::ScriptEngine::RegisterNativeHandler("MUMBLE_SET_VOICE_CHANNEL", MakeMumbleNative([](fx::ScriptContext& context) { - if (g_mumble.connected) - { - g_mumbleClient->SetChannel(fmt::sprintf("Game Channel %d", context.GetArgument(0))); - } - }); + g_mumbleClient->SetChannel(GetMumbleChannel(context.GetArgument(0))); + })); scrBindGlobal("GET_AUDIOCONTEXT_FOR_CLIENT", getAudioContext); scrBindGlobal("GET_AUDIOCONTEXT_FOR_SERVERID", getAudioContextByServerId); @@ -1329,7 +1374,7 @@ static HookFunction hookFunction([]() fx::ScriptEngine::RegisterNativeHandler(0x031E11F3D447647E, [=](fx::ScriptContext& context) { - if (!g_mumble.connected) + if (!IsMumbleConnected()) { origIsTalking(context); return; @@ -1353,9 +1398,9 @@ static HookFunction hookFunction([]() { origSetChannel(context); - if (g_mumble.connected) + if (IsMumbleConnected()) { - g_mumbleClient->SetChannel(fmt::sprintf("Game Channel %d", context.GetArgument(0))); + g_mumbleClient->SetChannel(GetMumbleChannel(context.GetArgument(0))); } }); @@ -1363,7 +1408,7 @@ static HookFunction hookFunction([]() { origClearChannel(context); - if (g_mumble.connected) + if (IsMumbleConnected()) { g_mumbleClient->SetChannel("Root"); } diff --git a/code/components/voip-mumble/include/MumbleAudioSink.h b/code/components/voip-mumble/include/MumbleAudioSink.h index ad52eb1976..db9b614387 100644 --- a/code/components/voip-mumble/include/MumbleAudioSink.h +++ b/code/components/voip-mumble/include/MumbleAudioSink.h @@ -20,7 +20,7 @@ DLL_IMPORT #else DLL_EXPORT #endif -fwEvent*> +fwEvent*> OnGetMumbleAudioSink; extern diff --git a/code/components/voip-mumble/include/MumbleClient.h b/code/components/voip-mumble/include/MumbleClient.h index f0bbda637c..a2cb92d12f 100644 --- a/code/components/voip-mumble/include/MumbleClient.h +++ b/code/components/voip-mumble/include/MumbleClient.h @@ -49,22 +49,8 @@ enum class MumbleVoiceLikelihood struct VoiceTargetConfig { - struct Target - { - std::vector users; - std::string channel; - // ACL is not supported in umurmur, so does not count - bool links; - bool children; - - inline Target() - : links(false), children(false) - { - - } - }; - - std::list targets; + std::set users; + std::set channels; }; class IMumbleClient : public fwRefCountable @@ -89,11 +75,11 @@ class IMumbleClient : public fwRefCountable virtual void SetChannel(const std::string& channelName) = 0; - virtual void SetClientVolumeOverride(const std::wstring& clientName, float volume) = 0; + virtual void SetClientVolumeOverride(const std::string& clientName, float volume) = 0; virtual void SetClientVolumeOverrideByServerId(uint32_t serverId, float volume) = 0; - virtual std::wstring GetPlayerNameFromServerId(uint32_t serverId) = 0; + virtual std::string GetPlayerNameFromServerId(uint32_t serverId) = 0; virtual std::string GetVoiceChannelFromServerId(uint32_t serverId) = 0; diff --git a/code/components/voip-mumble/include/MumbleClientImpl.h b/code/components/voip-mumble/include/MumbleClientImpl.h index 6d49dfeae7..46b2686345 100644 --- a/code/components/voip-mumble/include/MumbleClientImpl.h +++ b/code/components/voip-mumble/include/MumbleClientImpl.h @@ -148,11 +148,11 @@ class MumbleClient : public IMumbleClient, public Botan::TLS::Callbacks virtual std::shared_ptr GetAudioContext(const std::string& name) override; - virtual void SetClientVolumeOverride(const std::wstring& clientName, float volume) override; + virtual void SetClientVolumeOverride(const std::string& clientName, float volume) override; virtual void SetClientVolumeOverrideByServerId(uint32_t serverId, float volume) override; - virtual std::wstring GetPlayerNameFromServerId(uint32_t serverId) override; + virtual std::string GetPlayerNameFromServerId(uint32_t serverId) override; virtual std::string GetVoiceChannelFromServerId(uint32_t serverId) override; diff --git a/code/components/voip-mumble/include/MumbleClientState.h b/code/components/voip-mumble/include/MumbleClientState.h index 5ab47286b2..18ef665971 100644 --- a/code/components/voip-mumble/include/MumbleClientState.h +++ b/code/components/voip-mumble/include/MumbleClientState.h @@ -19,11 +19,11 @@ class MumbleChannel MumbleClient* m_client; - std::wstring m_channelName; + std::string m_channelName; bool m_hasDescription; - std::wstring m_channelDescription; + std::string m_channelDescription; std::string m_descriptionHash; @@ -32,11 +32,11 @@ class MumbleChannel public: MumbleChannel(MumbleClient* client, MumbleProto::ChannelState& channelState); - inline std::wstring GetName() const { return m_channelName; } + inline std::string GetName() const { return m_channelName; } inline bool HasDescription() const { return m_hasDescription; } - inline std::wstring GetDescription() const { return m_channelDescription; } + inline std::string GetDescription() const { return m_channelDescription; } inline bool IsTemporary() const { return m_temporary; } @@ -52,7 +52,7 @@ class MumbleUser uint32_t m_serverId; - std::wstring m_name; + std::string m_name; uint32_t m_currentChannelId; @@ -78,7 +78,7 @@ class MumbleUser inline uint32_t GetServerId() const { return m_serverId; } - inline std::wstring GetName() const { return m_name; } + inline std::string GetName() const { return m_name; } inline uint32_t GetChannelId() const { return m_currentChannelId; } @@ -92,7 +92,7 @@ class MumbleClientState uint32_t m_session; - std::wstring m_username; + std::string m_username; std::map m_channels; @@ -105,16 +105,16 @@ class MumbleClientState { m_client = nullptr; m_session = 0; - m_username = L""; + m_username = ""; m_channels.clear(); m_users.clear(); } inline void SetClient(MumbleClient* client) { m_client = client; } - inline void SetUsername(std::wstring& value) { m_username = value; } + inline void SetUsername(const std::string& value) { m_username = value; } - inline std::wstring GetUsername() { return m_username; } + inline std::string GetUsername() { return m_username; } inline void SetSession(uint32_t sessionId) { m_session = sessionId; } diff --git a/code/components/voip-mumble/src/MumbleAudioOutput.cpp b/code/components/voip-mumble/src/MumbleAudioOutput.cpp index 090ec5a2d7..39e3e81790 100644 --- a/code/components/voip-mumble/src/MumbleAudioOutput.cpp +++ b/code/components/voip-mumble/src/MumbleAudioOutput.cpp @@ -528,7 +528,7 @@ MumbleAudioOutput::ClientAudioState::~ClientAudioState() } DLL_EXPORT -fwEvent*> +fwEvent*> OnGetMumbleAudioSink; DLL_EXPORT @@ -1508,7 +1508,7 @@ void MumbleAudioOutput::SetAudioDevice(const std::string& deviceId) m_client->GetState().ForAllUsers([this](const std::shared_ptr& user) { - HandleClientConnect(*user); + HandleClientConnect(*user); }); } @@ -1553,8 +1553,6 @@ void DuckingOptOut(WRL::ComPtr device); std::shared_ptr MumbleAudioOutput::GetAudioContext(const std::string& name) { - auto wideName = ToWide(name); - std::shared_lock _(m_clientsMutex); for (auto& client : m_clients) @@ -1566,7 +1564,7 @@ std::shared_ptr MumbleAudioOutput::GetAudioContext(const std: auto user = m_client->GetState().GetUser(client.first); - if (!user || user->GetName() != wideName) + if (!user || user->GetName() != name) { continue; } diff --git a/code/components/voip-mumble/src/MumbleClient.cpp b/code/components/voip-mumble/src/MumbleClient.cpp index d4d5044e5a..6a42f497f0 100644 --- a/code/components/voip-mumble/src/MumbleClient.cpp +++ b/code/components/voip-mumble/src/MumbleClient.cpp @@ -24,7 +24,7 @@ static __declspec(thread) MumbleClient* g_currentMumbleClient; using namespace std::chrono_literals; -constexpr auto kUDPPingInterval = 1000ms; +constexpr auto kPingInterval = 1000ms; constexpr uint16_t kMaxUdpPacket = 1024; inline std::chrono::milliseconds msec() @@ -115,6 +115,7 @@ void MumbleClient::Initialize() // don't start idle timer here - it should only start after TLS handshake is done! m_timeSinceJoin = msec(); + m_inFlightTcpPings = 0; m_connectionInfo.isConnected = true; }); @@ -159,13 +160,12 @@ void MumbleClient::Initialize() m_tcp->connect(*address.GetSocketAddress()); m_state.Reset(); m_state.SetClient(this); - m_state.SetUsername(ToWide(m_connectionInfo.username)); + m_state.SetUsername(m_connectionInfo.username); }); m_idleTimer = m_loop->Get()->resource(); m_idleTimer->on([this](const uvw::TimerEvent& ev, uvw::TimerHandle& t) { - auto lockedIsActive = [this]() { std::unique_lock _(m_clientMutex); @@ -178,15 +178,14 @@ void MumbleClient::Initialize() { if (m_curManualChannel != m_lastManualChannel && !m_state.GetChannels().empty()) { - // check if the channel already exists - std::wstring wname = ToWide(m_curManualChannel); m_lastManualChannel = m_curManualChannel; bool existed = false; + // check if the channel already exists, if it does set us to the channel for (const auto& channel : m_state.GetChannels()) { - if (channel.second.GetName() == wname) + if (channel.second.GetName() == m_curManualChannel) { // join the channel MumbleProto::UserState state; @@ -224,11 +223,9 @@ void MumbleClient::Initialize() auto findCh = [&](const std::string& ch) { - std::wstring wname = ToWide(ch); - for (const auto& channel : m_state.GetChannels()) { - if (channel.second.GetName() == wname) + if (channel.second.GetName() == ch) { return channel.first; } @@ -287,35 +284,31 @@ void MumbleClient::Initialize() MumbleProto::VoiceTarget target; target.set_id(idx); - for (auto& t : config.targets) + // Voice targets can all be set in a single target + auto vt = target.add_targets(); + for (auto& userName : config.users) { - auto vt = target.add_targets(); - - for (auto& userName : t.users) + m_state.ForAllUsers([this, &userName, &vt](const std::shared_ptr& user) { - m_state.ForAllUsers([this, &userName, &vt](const std::shared_ptr& user) + if (user->GetName() == userName) { - if (user->GetName() == userName) - { - vt->add_session(user->GetSessionId()); - } - }); - } + vt->add_session(user->GetSessionId()); + } + }); + } + - if (!t.channel.empty()) + for (auto& channelName: config.channels) + { + for (auto& channelPair : m_state.GetChannels()) { - std::wstring wname = ToWide(t.channel); - for (auto& channelPair : m_state.GetChannels()) + if (channelPair.second.GetName() == channelName) { - if (channelPair.second.GetName() == wname) - { - vt->set_channel_id(channelPair.first); - } + // Channel targeting happens per channel, so we need to add a new target per channel + auto vt = target.add_targets(); + vt->set_channel_id(channelPair.first); } } - - vt->set_links(t.links); - vt->set_children(t.children); } Send(MumbleMessageType::VoiceTarget, target); @@ -337,7 +330,7 @@ void MumbleClient::Initialize() if (!name.empty()) { - m_lastManualChannel = ToNarrow(name); + m_lastManualChannel = name; } } } @@ -345,10 +338,13 @@ void MumbleClient::Initialize() if (msec() > m_nextPing) { { - // only log once at 4 pings - if (m_inFlightTcpPings == 4) + // reset the connection when we're at more than 4 pings (which will be about 4 seconds) and we haven't just connected + if (m_inFlightTcpPings >= 4 && (msec() - m_timeSinceJoin) > 20s) { - console::PrintWarning("mumble", "Server is not responding to TCP pings\n"); + // Reset our connection status so that mumble will try to reconnect us + m_connectionInfo.isConnected = false; + m_connectionInfo.isConnecting = false; + console::PrintWarning("mumble", "Server is not responding to TCP pings after 4 seconds, resetting connection\n"); } m_inFlightTcpPings += 1; @@ -383,7 +379,7 @@ void MumbleClient::Initialize() SendUDP(pingBuf, pds.size()); } - m_nextPing = msec() + kUDPPingInterval; + m_nextPing = msec() + kPingInterval; } } else if (m_connectionInfo.address.GetAddressFamily() != 0) @@ -422,11 +418,10 @@ concurrency::task MumbleClient::ConnectAsync(const net::P m_tcpPingCount = 0; - memset(m_tcpPings, 0, sizeof(m_tcpPings)); m_state.SetClient(this); - m_state.SetUsername(ToWide(userName)); + m_state.SetUsername(userName); m_loop->EnqueueCallback([this]() { @@ -590,7 +585,7 @@ float MumbleClient::GetInputAudioLevel() return m_audioInput.GetAudioLevel(); } -void MumbleClient::SetClientVolumeOverride(const std::wstring& clientName, float volume) +void MumbleClient::SetClientVolumeOverride(const std::string& clientName, float volume) { m_state.ForAllUsers([this, &clientName, volume](const std::shared_ptr& user) { @@ -612,9 +607,9 @@ void MumbleClient::SetClientVolumeOverrideByServerId(uint32_t serverId, float vo }); } -std::wstring MumbleClient::GetPlayerNameFromServerId(uint32_t serverId) +std::string MumbleClient::GetPlayerNameFromServerId(uint32_t serverId) { - std::wstring retName; + std::string retName; m_state.ForAllUsers([serverId, &retName](const std::shared_ptr& user) { @@ -634,10 +629,11 @@ std::wstring MumbleClient::GetPlayerNameFromServerId(uint32_t serverId) std::string MumbleClient::GetVoiceChannelFromServerId(uint32_t serverId) { - std::string retString = ""; + std::string retString; m_state.ForAllUsers([this, serverId, &retString](const std::shared_ptr& user) { + // if we already have a name we can ignore and bail if (!retString.empty()) { return; @@ -651,7 +647,7 @@ std::string MumbleClient::GetVoiceChannelFromServerId(uint32_t serverId) if (chit != channels.end()) { - retString = ToNarrow(chit->second.GetName()); + retString = chit->second.GetName(); } } }); @@ -661,11 +657,9 @@ std::string MumbleClient::GetVoiceChannelFromServerId(uint32_t serverId) bool MumbleClient::DoesChannelExist(const std::string& channelName) { - std::wstring wname = ToWide(channelName); - for (const auto& channel : m_state.GetChannels()) { - if (channel.second.GetName() == wname) + if (channel.second.GetName() == channelName) { return true; } @@ -687,14 +681,14 @@ void MumbleClient::GetTalkers(std::vector* referenceIds) if (user) { - referenceIds->push_back(ToNarrow(user->GetName())); + referenceIds->push_back(user->GetName()); } } // local talker talking? if (m_audioInput.IsTalking()) { - referenceIds->push_back(ToNarrow(m_state.GetUsername())); + referenceIds->push_back(m_state.GetUsername()); } } @@ -775,7 +769,6 @@ void MumbleClient::HandleUDP(const uint8_t* buf, size_t size) return; } - // handle voice packet HandleVoice(outBuf, size - 4); } @@ -928,7 +921,7 @@ void MumbleClient::RunFrame() { if (m_positionHook) { - auto newPos = m_positionHook(ToNarrow(user->GetName())); + auto newPos = m_positionHook(user->GetName()); if (newPos) { @@ -1102,14 +1095,25 @@ void MumbleClient::OnActivated() // the idle event would immediately try to reconnect) m_idleTimer->start(500ms, 500ms); - // send our own version + // https://github.com/mumble-voip/mumble/blob/master/docs/dev/network-protocol/establishing_connection.md#version-exchange + // Send our version whenever our TLS Session gets initialized MumbleProto::Version ourVersion; ourVersion.set_version(0x00010204); ourVersion.set_os("Windows"); ourVersion.set_os_version("Cfx/Embedded"); ourVersion.set_release("CitizenFX Client"); - this->Send(MumbleMessageType::Version, ourVersion); + Send(MumbleMessageType::Version, ourVersion); + + // https://github.com/mumble-voip/mumble/blob/master/docs/dev/network-protocol/establishing_connection.md#authenticate + // Send our auth packet immediately after + auto username = GetState().GetUsername(); + + MumbleProto::Authenticate authenticate; + authenticate.set_opus(true); + authenticate.set_username(username); + + Send(MumbleMessageType::Authenticate, authenticate); } fwRefContainer MumbleClient::GetCurrent() diff --git a/code/components/voip-mumble/src/MumbleClientState_Channel.cpp b/code/components/voip-mumble/src/MumbleClientState_Channel.cpp index 7bd4010baa..4f7b97c044 100644 --- a/code/components/voip-mumble/src/MumbleClientState_Channel.cpp +++ b/code/components/voip-mumble/src/MumbleClientState_Channel.cpp @@ -30,12 +30,12 @@ void MumbleChannel::UpdateChannel(MumbleProto::ChannelState& state) if (state.has_name()) { - m_channelName = ConvertFromUTF8(state.name()); + m_channelName = state.name(); } if (state.has_description()) { - m_channelDescription = ConvertFromUTF8(state.description()); + m_channelDescription = state.description(); m_hasDescription = true; } diff --git a/code/components/voip-mumble/src/MumbleClientState_User.cpp b/code/components/voip-mumble/src/MumbleClientState_User.cpp index 2d5cf091ba..a02454f324 100644 --- a/code/components/voip-mumble/src/MumbleClientState_User.cpp +++ b/code/components/voip-mumble/src/MumbleClientState_User.cpp @@ -35,11 +35,10 @@ void MumbleUser::UpdateUser(MumbleProto::UserState& state) if (state.has_name()) { - std::string name = state.name(); - m_name = ConvertFromUTF8(name); - if (name.length() >= 2) + m_name = state.name(); + if (m_name.length() >= 2) { - m_serverId = atoi(name.substr(1, name.length() - 1).c_str()); + m_serverId = atoi(m_name.substr(1, m_name.length() - 1).c_str()); } else { @@ -77,7 +76,7 @@ void MumbleUser::UpdateUser(MumbleProto::UserState& state) m_currentChannelId = state.channel_id(); } - console::DPrintf("mumble", "%s joined channel %d\n", ConvertToUTF8(m_name), m_currentChannelId); + console::DPrintf("mumble", "%s joined channel %d\n", m_name, m_currentChannelId); } void MumbleClientState::ProcessUserState(MumbleProto::UserState& userState) @@ -100,7 +99,7 @@ void MumbleClientState::ProcessUserState(MumbleProto::UserState& userState) createdUser = user; - console::DPrintf("Mumble", "New user: %s\n", ToNarrow(user->GetName())); + console::DPrintf("Mumble", "New user: %s\n", user->GetName()); } else { diff --git a/code/components/voip-mumble/src/MumbleVersionHandler.cpp b/code/components/voip-mumble/src/MumbleVersionHandler.cpp index f73341b07f..d0056d6a08 100644 --- a/code/components/voip-mumble/src/MumbleVersionHandler.cpp +++ b/code/components/voip-mumble/src/MumbleVersionHandler.cpp @@ -13,15 +13,5 @@ DEFINE_HANDLER(Version) { auto client = MumbleClient::GetCurrent(); - // also send our initial registration packet - auto username = client->GetState().GetUsername(); - auto usernameUtf8 = ConvertToUTF8(username); - - MumbleProto::Authenticate authenticate; - authenticate.set_opus(true); - authenticate.set_username(usernameUtf8); - - client->Send(MumbleMessageType::Authenticate, authenticate); - client->EnableAudioInput(); }); diff --git a/ext/native-decls/MumbleSetServerAddress.md b/ext/native-decls/MumbleSetServerAddress.md index 1f96c2af97..81fcb263b2 100644 --- a/ext/native-decls/MumbleSetServerAddress.md +++ b/ext/native-decls/MumbleSetServerAddress.md @@ -10,6 +10,8 @@ void MUMBLE_SET_SERVER_ADDRESS(char* address, int port); Changes the Mumble server address to connect to, and reconnects to the new address. +Setting the address to an empty string and the port to -1 will reset to the built in FXServer Mumble Implementation. + ## Parameters * **address**: The address of the mumble server. * **port**: The port of the mumble server.