diff --git a/.gitignore b/.gitignore index 3b2390e4bf0..96a70250688 100644 --- a/.gitignore +++ b/.gitignore @@ -95,4 +95,7 @@ cmake_install.cmake # # Module files # -src/modules/ \ No newline at end of file +src/modules/ + +# Voice Chat server +src/voicechat-server/ diff --git a/cmake/options.cmake b/cmake/options.cmake index e180ee0e702..7018079687f 100644 --- a/cmake/options.cmake +++ b/cmake/options.cmake @@ -8,6 +8,7 @@ option(BUILD_EXTRACTORS "Build map/dbc/vmap/mmap extractors" option(BUILD_SCRIPTDEV "Build ScriptDev. (OFF Speedup build)" ON) option(BUILD_PLAYERBOTS "Build Playerbots mod" OFF) option(BUILD_AHBOT "Build Auction House Bot mod" OFF) +option(BUILD_VOICECHAT "Build VoiceChat server and handlers" OFF) option(BUILD_METRICS "Build Metrics, generate data for Grafana" OFF) option(BUILD_RECASTDEMOMOD "Build map/vmap/mmap viewer" OFF) option(BUILD_GIT_ID "Build git_id" OFF) @@ -36,6 +37,7 @@ message(STATUS BUILD_EXTRACTORS Build map/dbc/vmap/mmap extractor BUILD_PLAYERBOTS Build Playerbots mod BUILD_AHBOT Build Auction House Bot mod + BUILD_VOICECHAT Build VoiceChat server and handlers BUILD_METRICS Build Metrics, generate data for Grafana BUILD_RECASTDEMOMOD Build map/vmap/mmap viewer BUILD_GIT_ID Build git_id diff --git a/cmake/showoptions.cmake b/cmake/showoptions.cmake index 7918ceceeb4..667ba394bfb 100644 --- a/cmake/showoptions.cmake +++ b/cmake/showoptions.cmake @@ -55,6 +55,12 @@ else() message(STATUS "Build AHBot : No (default)") endif() +if(BUILD_VOICECHAT) + message(STATUS "Build VoiceChat : Yes") +else() + message(STATUS "Build VoiceChat : No (default)") +endif() + if(BUILD_METRICS) message(STATUS "Build METRICS : Yes") else() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ddde74c2e2b..69486ab1eff 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -57,4 +57,34 @@ endif() if(BUILD_LOGIN_SERVER) add_subdirectory(realmd) -endif() \ No newline at end of file +endif() + +# Voice Chat server and handlers +if(BUILD_VOICECHAT) + include(FetchContent) + + FetchContent_Declare( + voicechat-server + GIT_REPOSITORY "https://github.com/celguar/voicechat-server.git" + GIT_TAG "main" + ) + + if(NOT voicechat-server_POPULATED) + message(STATUS "Fetching VoiceChat server...") + + FetchContent_Populate(voicechat-server) + + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/voicechat-server) + file(REMOVE_RECURSE ${CMAKE_CURRENT_SOURCE_DIR}/voicechat-server) + endif() + + file(COPY ${voicechat-server_SOURCE_DIR}/. DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/voicechat-server) + message(STATUS "VoiceChat Server fetched and populated in ${CMAKE_CURRENT_SOURCE_DIR}/voicechat-server") + endif() + + add_subdirectory(voicechat-server) +else() + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/voicechat-server) + file(REMOVE_RECURSE ${CMAKE_CURRENT_SOURCE_DIR}/voicechat-server) + endif() +endif() diff --git a/src/game/CMakeLists.txt b/src/game/CMakeLists.txt index 45a1d33b1fe..958847e01cd 100644 --- a/src/game/CMakeLists.txt +++ b/src/game/CMakeLists.txt @@ -40,6 +40,16 @@ if(NOT BUILD_SCRIPTDEV) endforeach() endif() +if(NOT BUILD_VOICECHAT) + set (EXCLUDE_DIR "VoiceChat") + foreach (TMP_PATH ${LIBRARY_SRCS}) + string (FIND ${TMP_PATH} ${EXCLUDE_DIR} EXCLUDE_DIR_FOUND) + if (NOT ${EXCLUDE_DIR_FOUND} EQUAL -1) + list(REMOVE_ITEM LIBRARY_SRCS ${TMP_PATH}) + endif () + endforeach() +endif() + if(NOT BUILD_DEPRECATED_PLAYERBOT) # exclude Playerbot folder set (EXCLUDE_DIR "PlayerBot/") @@ -145,6 +155,11 @@ if (BUILD_AHBOT) add_definitions(-DBUILD_AHBOT) endif() +# Define BUILD_VOICECHAT if need +if (BUILD_VOICECHAT) + add_definitions(-DBUILD_VOICECHAT) +endif() + # Define BUILD_METRICS if need if (BUILD_METRICS) add_definitions(-DBUILD_METRICS) diff --git a/src/game/Chat/Channel.cpp b/src/game/Chat/Channel.cpp index 17b827027e5..f972be57e64 100644 --- a/src/game/Chat/Channel.cpp +++ b/src/game/Chat/Channel.cpp @@ -23,8 +23,15 @@ #include "Chat/Chat.h" #include "Anticheat/Anticheat.hpp" +#ifdef BUILD_VOICECHAT +#include "VoiceChat/VoiceChatMgr.h" +#endif + Channel::Channel(const std::string& name, uint32 channel_id/* = 0*/) : m_name(name) +#ifdef BUILD_VOICECHAT + , m_voice(false) +#endif { if (ChatChannelsEntry const* builtin = GetChatChannelsEntryFor(name, channel_id)) { @@ -122,6 +129,27 @@ void Channel::Join(Player* player, const char* password) pinfo.player = guid; pinfo.flags = MEMBER_FLAG_NONE; +#ifdef BUILD_VOICECHAT + // join voice chat + if (!IsConstant() && HasFlag(CHANNEL_FLAG_CUSTOM) && sVoiceChatMgr.CanUseVoiceChat()) + { + // first voice chat enabled member turns it on + // only proof is https://www.youtube.com/watch?v=h5oH4ER2cJ0 where voice chat is auto enabled on new channel + if (!IsVoiceEnabled()) + { + if (player->GetSession()->IsVoiceChatEnabled()) + { + // toggle voice and update player flags + ToggleVoice(player); + } + } + if (IsVoiceEnabled()) + { + sVoiceChatMgr.AddToCustomVoiceChatChannel(guid, this->GetName(), player->GetTeam()); + } + } +#endif + MakeYouJoined(data, m_name, *this); SendToOne(data, guid); @@ -178,6 +206,14 @@ void Channel::Leave(Player* player, bool send) if (changeowner && !IsPublic()) SetOwner(SelectNewOwner(), (m_players.size() > 1)); + +#ifdef BUILD_VOICECHAT + // leave voice chat + if (IsVoiceEnabled()) + { + sVoiceChatMgr.RemoveFromCustomVoiceChatChannel(guid, this->GetName(), player->GetTeam()); + } +#endif } void Channel::KickOrBan(Player* player, const char* targetName, bool ban) @@ -333,7 +369,7 @@ void Channel::SetPassword(Player* player, const char* password) void Channel::SetModeFlags(Player* player, const char* targetName, ChannelMemberFlags flags, bool set) { // Restrict input flags to currently supported by this method - flags = ChannelMemberFlags(uint8(flags) & (MEMBER_FLAG_MODERATOR | MEMBER_FLAG_MUTED)); + flags = ChannelMemberFlags(uint8(flags) & (MEMBER_FLAG_MODERATOR | MEMBER_FLAG_MIC_MUTED | MEMBER_FLAG_MIC_MUTED | MEMBER_FLAG_VOICED)); if (!flags) return; @@ -766,6 +802,95 @@ void Channel::DeVoice(ObjectGuid /*guid1*/, ObjectGuid /*guid2*/) const { } +#ifdef BUILD_VOICECHAT +void Channel::AddVoiceChatMembersAfterCreate() +{ + // add voice enabled players to channel after it's created on voice server + for (auto itr = m_players.begin(); itr != m_players.end(); ++itr) + { + if (Player* plr = sObjectMgr.GetPlayer(itr->first, false)) + { + if (plr->GetSession()->IsVoiceChatEnabled()) + { + sVoiceChatMgr.AddToCustomVoiceChatChannel(itr->first, this->GetName(), plr->GetTeam()); + } + } + } +} + +void Channel::ToggleVoice(Player* player) +{ + // silently disable if voice server disconnected + if (!player) + { + m_voice = !m_voice; + if (m_voice) + m_flags |= CHANNEL_FLAG_VOICE; + else + m_flags &= ~CHANNEL_FLAG_VOICE; + + return; + } + + ObjectGuid guid = player->GetObjectGuid(); + + if (!IsOn(guid)) + { + WorldPacket data; + MakeNotMember(data, m_name); + SendToOne(data, guid); + return; + } + + const uint32 level = sWorld.getConfig(CONFIG_UINT32_GM_LEVEL_CHANNEL_MODERATION); + const bool gm = (level && player->GetSession()->GetSecurity() >= level); + + if (!m_players[guid].IsOwner() && !gm) + { + WorldPacket data; + MakeNotOwner(data, m_name); + SendToOne(data, guid); + return; + } + + // toggle channel voice + m_voice = !m_voice; + + WorldPacket data; + if (m_voice) + MakeVoiceOn(data, m_name, guid); + else + MakeVoiceOff(data, m_name, guid); + + SendToAll(data); + + if (m_voice) + m_flags |= CHANNEL_FLAG_VOICE; + else + m_flags &= ~CHANNEL_FLAG_VOICE; + + // update player flags, maybe used in right click menu in chat UI + for (PlayerList::const_iterator i = m_players.begin(); i != m_players.end(); ++i) + { + if (Player* member = sObjectMgr.GetPlayer(i->first, false)) + { + if (WorldSession* session = member->GetSession()) + { + if (session->IsVoiceChatEnabled()) + { + m_players[i->first].SetVoiced(m_voice); + } + } + } + } + + if (m_voice) + { + sVoiceChatMgr.AddToCustomVoiceChatChannel(guid, this->GetName(), player->GetTeam()); + } +} +#endif + void Channel::JoinNotify(ObjectGuid guid) { WorldPacket data; @@ -1033,7 +1158,7 @@ ObjectGuid Channel::SelectNewOwner() const void Channel::SetModeFlags(ObjectGuid guid, ChannelMemberFlags flags, bool set) { // Restrict input flags to currently supported by this method - flags = ChannelMemberFlags(uint8(flags) & (MEMBER_FLAG_MODERATOR | MEMBER_FLAG_MUTED)); + flags = ChannelMemberFlags(uint8(flags) & (MEMBER_FLAG_MODERATOR | MEMBER_FLAG_MIC_MUTED | MEMBER_FLAG_MIC_MUTED | MEMBER_FLAG_VOICED)); if (flags && m_players[guid].HasFlag(flags) != set) { diff --git a/src/game/Chat/Channel.h b/src/game/Chat/Channel.h index 1b54f5e92e9..6e740ee4171 100644 --- a/src/game/Chat/Channel.h +++ b/src/game/Chat/Channel.h @@ -131,6 +131,12 @@ class Channel inline void SetModerator(bool state) { SetFlag(MEMBER_FLAG_MODERATOR, state); } inline bool IsMuted() const { return HasFlag(MEMBER_FLAG_MUTED); } inline void SetMuted(bool state) { SetFlag(MEMBER_FLAG_MUTED, state); } +#ifdef BUILD_VOICECHAT + inline bool IsMicMuted() const { return HasFlag(MEMBER_FLAG_MIC_MUTED); } + inline void SetMicMuted(bool state) { SetFlag(MEMBER_FLAG_MIC_MUTED, state); } + inline bool IsVoiced() const { return HasFlag(MEMBER_FLAG_VOICED); } + inline void SetVoiced(bool state) { SetFlag(MEMBER_FLAG_VOICED, state); } +#endif }; typedef std::map PlayerList; @@ -159,6 +165,10 @@ class Channel void SetModeFlags(Player* player, const char* targetName, ChannelMemberFlags flags, bool set); inline void SetModerator(Player* player, const char* targetName, bool set) { SetModeFlags(player, targetName, MEMBER_FLAG_MODERATOR, set); } inline void SetMute(Player* player, const char* targetName, bool set) { SetModeFlags(player, targetName, MEMBER_FLAG_MUTED, set); } +#ifdef BUILD_VOICECHAT + inline void SetMicMute(Player* player, const char* targetName, bool set) { SetModeFlags(player, targetName, MEMBER_FLAG_MIC_MUTED, set); } + inline void SetVoiced(Player* player, const char* targetName, bool set) { SetModeFlags(player, targetName, MEMBER_FLAG_VOICED, set); } +#endif void SetOwner(Player* player, const char* targetName); void SendChannelOwnerResponse(Player* player) const; void SendChannelListResponse(Player* player, bool display = false); @@ -171,6 +181,12 @@ class Channel void JoinNotify(ObjectGuid guid); // invisible notify void LeaveNotify(ObjectGuid guid); // invisible notify +#ifdef BUILD_VOICECHAT + void AddVoiceChatMembersAfterCreate(); + void ToggleVoice(Player* player = nullptr); + bool IsVoiceEnabled() const { return HasFlag(CHANNEL_FLAG_VOICE); } +#endif + // initial packet data (notify type and channel name) static void MakeNotifyPacket(WorldPacket& data, const std::string& channel, ChatNotify type); // type specific packet data @@ -214,14 +230,14 @@ class Channel // Make a custom channel acquire global-like properties bool SetStatic(bool state, bool command = false); + bool IsOn(ObjectGuid who) const { return m_players.find(who) != m_players.end(); } + bool IsBanned(ObjectGuid guid) const { return m_banned.find(guid) != m_banned.end(); } + private: void SendToOne(WorldPacket const& data, ObjectGuid receiver) const; void SendToAll(WorldPacket const& data) const; void SendMessage(WorldPacket const& data, ObjectGuid sender) const; - bool IsOn(ObjectGuid who) const { return m_players.find(who) != m_players.end(); } - bool IsBanned(ObjectGuid guid) const { return m_banned.find(guid) != m_banned.end(); } - uint8 GetPlayerFlags(ObjectGuid guid) const { PlayerList::const_iterator p_itr = m_players.find(guid); @@ -245,6 +261,9 @@ class Channel const ChatChannelsEntry* m_entry = nullptr; bool m_announcements = false; bool m_moderation = false; +#ifdef BUILD_VOICECHAT + bool m_voice = false; +#endif uint8 m_flags = CHANNEL_FLAG_NONE; // Custom features: bool m_static = false; diff --git a/src/game/Chat/ChannelMgr.cpp b/src/game/Chat/ChannelMgr.cpp index fe59b25b4c7..0472805c21d 100644 --- a/src/game/Chat/ChannelMgr.cpp +++ b/src/game/Chat/ChannelMgr.cpp @@ -20,6 +20,10 @@ #include "Policies/Singleton.h" #include "World/World.h" +#ifdef BUILD_VOICECHAT +#include "VoiceChat/VoiceChatMgr.h" +#endif + INSTANTIATE_SINGLETON_1(AllianceChannelMgr); INSTANTIATE_SINGLETON_1(HordeChannelMgr); @@ -97,6 +101,12 @@ void ChannelMgr::LeftChannel(const std::string& name) if (channel->GetNumPlayers() == 0 && !channel->IsConstant()) { +#ifdef BUILD_VOICECHAT + // delete voice channel + Team team = this == channelMgr(ALLIANCE) ? ALLIANCE : HORDE; + sVoiceChatMgr.DeleteCustomVoiceChatChannel(channel->GetName(), team); +#endif + channels.erase(wname); delete channel; } diff --git a/src/game/Chat/Chat.cpp b/src/game/Chat/Chat.cpp index f4b63b90d03..6f0ffb6a7f3 100644 --- a/src/game/Chat/Chat.cpp +++ b/src/game/Chat/Chat.cpp @@ -942,6 +942,17 @@ ChatCommand* ChatHandler::getCommandTable() { nullptr, 0, false, nullptr, "", nullptr } }; +#ifdef BUILD_VOICECHAT + static ChatCommand voiceChatCommandTable[] = + { + { "disconnect", SEC_ADMINISTRATOR, false, &ChatHandler::HandleVoiceChatDisconnectCommand, "", nullptr }, + { "disable", SEC_ADMINISTRATOR, false, &ChatHandler::HandleVoiceChatDisableCommand, "", nullptr }, + { "enable", SEC_ADMINISTRATOR, false, &ChatHandler::HandleVoiceChatEnableCommand, "", nullptr }, + { "stats", SEC_ADMINISTRATOR, false, &ChatHandler::HandleVoiceChatStatsCommand, "", nullptr }, + { nullptr, 0, false, nullptr, "", nullptr } + }; +#endif + static ChatCommand commandTable[] = { { "anticheat", SEC_GAMEMASTER, true, nullptr, "", anticheatCommandTable}, @@ -1056,6 +1067,9 @@ ChatCommand* ChatHandler::getCommandTable() { "loot", SEC_GAMEMASTER, true, nullptr, "", lootCommandTable }, #ifdef BUILD_DEPRECATED_PLAYERBOT { "bot", SEC_PLAYER, false, &ChatHandler::HandlePlayerbotCommand, "", nullptr }, +#endif +#ifdef BUILD_VOICECHAT + { "voicechat", SEC_ADMINISTRATOR, false, nullptr, "", voiceChatCommandTable }, #endif { nullptr, 0, false, nullptr, "", nullptr } }; diff --git a/src/game/Chat/Chat.h b/src/game/Chat/Chat.h index 7233d8d6bd3..84f112868af 100644 --- a/src/game/Chat/Chat.h +++ b/src/game/Chat/Chat.h @@ -818,6 +818,14 @@ class ChatHandler bool HandleBattlegroundStartCommand(char* args); bool HandleBattlegroundStopCommand(char* args); +#ifdef BUILD_VOICECHAT + // Voice Chat + bool HandleVoiceChatDisconnectCommand(char* args); + bool HandleVoiceChatDisableCommand(char* args); + bool HandleVoiceChatEnableCommand(char* args); + bool HandleVoiceChatStatsCommand(char* args); +#endif + //! Development Commands bool HandleSaveAllCommand(char* args); diff --git a/src/game/Chat/Level3.cpp b/src/game/Chat/Level3.cpp index fbc25fc427e..f5b35e3585b 100644 --- a/src/game/Chat/Level3.cpp +++ b/src/game/Chat/Level3.cpp @@ -182,6 +182,75 @@ bool ChatHandler::HandleAHBotItemCommand(char* args) } #endif +#ifdef BUILD_VOICECHAT +#include "VoiceChat/VoiceChatMgr.h" + +bool ChatHandler::HandleVoiceChatDisconnectCommand(char* args) +{ + if (!sVoiceChatMgr.CanUseVoiceChat()) + { + PSendSysMessage("Voice Chat is disabled or Voice Chat server is not connected!"); + return false; + } + + sVoiceChatMgr.SocketDisconnected(); + + int32 reconnectAttempts = sVoiceChatMgr.GetReconnectAttempts(); + if (reconnectAttempts == 0) + { + PSendSysMessage("Voice Chat server disconnected!"); + } + else if (reconnectAttempts < 0) + { + PSendSysMessage("Voice Chat server disconnected, reconnect enabled (infinite attempts)"); + } + else if (reconnectAttempts > 0) + { + PSendSysMessage("Voice Chat server disconnected, reconnect enabled (%u attempts)", reconnectAttempts); + } + return true; +} + +bool ChatHandler::HandleVoiceChatDisableCommand(char* args) +{ + if (!sVoiceChatMgr.IsEnabled()) + { + PSendSysMessage("Voice Chat is already disabled"); + return false; + } + + sVoiceChatMgr.DisableVoiceChat(); + PSendSysMessage("Voice Chat disabled!"); + return true; +} + +bool ChatHandler::HandleVoiceChatEnableCommand(char* args) +{ + if (sVoiceChatMgr.IsEnabled()) + { + PSendSysMessage("Voice Chat is already enabled"); + return false; + } + + sVoiceChatMgr.EnableVoiceChat(); + PSendSysMessage("Voice Chat enabled!"); + return true; +} + +bool ChatHandler::HandleVoiceChatStatsCommand(char* args) +{ + if (!sVoiceChatMgr.IsEnabled()) + { + PSendSysMessage("Voice Chat is disabled"); + return false; + } + + VoiceChatStatistics stats = sVoiceChatMgr.GetStatistics(); + PSendSysMessage("Voice Chat: channels: %u, active users: %u, voice chat enabled: %u, microphone enabled: %u", stats.channels, stats.active_users, stats.totalVoiceChatEnabled, stats.totalVoiceMicEnabled); + return true; +} +#endif + // reload commands bool ChatHandler::HandleReloadAllCommand(char* /*args*/) { diff --git a/src/game/Entities/CharacterHandler.cpp b/src/game/Entities/CharacterHandler.cpp index 46ff90cc3ce..6d517d819ba 100644 --- a/src/game/Entities/CharacterHandler.cpp +++ b/src/game/Entities/CharacterHandler.cpp @@ -40,6 +40,10 @@ #include "AI/ScriptDevAI/ScriptDevAIMgr.h" #include "Anticheat/Anticheat.hpp" +#ifdef BUILD_VOICECHAT +#include "VoiceChat/VoiceChatMgr.h" +#endif + #ifdef BUILD_DEPRECATED_PLAYERBOT #include "PlayerBot/Base/PlayerbotMgr.h" #endif @@ -789,7 +793,11 @@ void WorldSession::HandlePlayerLogin(LoginQueryHolder* holder) data.Initialize(SMSG_FEATURE_SYSTEM_STATUS, 2); // added in 2.2.0 data << uint8(2); // Can complain (0 = disabled, 1 = enabled, don't auto ignore, 2 = enabled, auto ignore) - data << uint8(0); // Voice chat is enabled +#ifdef BUILD_VOICECHAT + data << uint8(sVoiceChatMgr.CanSeeVoiceChat()); // Voice chat is available +#else + data << uint8(0); // Voice chat is disabled +#endif SendPacket(data); // Send Spam records @@ -989,6 +997,14 @@ void WorldSession::HandlePlayerLogin(LoginQueryHolder* holder) if (!pCurrChar->IsStandState() && !pCurrChar->IsStunned()) pCurrChar->SetStandState(UNIT_STAND_STATE_STAND); +#ifdef BUILD_VOICECHAT + // join available voice channels + if (IsVoiceChatEnabled()) + { + sVoiceChatMgr.JoinAvailableVoiceChatChannels(this); + } +#endif + m_playerLoading = false; delete holder; } @@ -1040,7 +1056,11 @@ void WorldSession::HandlePlayerReconnect() data.Initialize(SMSG_FEATURE_SYSTEM_STATUS, 2); // added in 2.2.0 data << uint8(2); // Can complain (0 = disabled, 1 = enabled, don't auto ignore, 2 = enabled, auto ignore) - data << uint8(0); // Voice chat is enabled +#ifdef BUILD_VOICECHAT + data << uint8(sVoiceChatMgr.CanSeeVoiceChat()); // Voice chat is available +#else + data << uint8(0); // Voice chat is disabled +#endif SendPacket(data); // Send Spam records @@ -1145,6 +1165,14 @@ void WorldSession::HandlePlayerReconnect() // Mark self for unit flags update to ensure re-application of combat flag at own client if (inCombat) _player->ForceValuesUpdateAtIndex(UNIT_FIELD_FLAGS); + + #ifdef BUILD_VOICECHAT + // join available voice channels + if (IsVoiceChatEnabled()) + { + sVoiceChatMgr.JoinAvailableVoiceChatChannels(this); + } +#endif m_playerLoading = false; } diff --git a/src/game/Groups/Group.cpp b/src/game/Groups/Group.cpp index 5f9972aa760..19e5d349c66 100644 --- a/src/game/Groups/Group.cpp +++ b/src/game/Groups/Group.cpp @@ -37,6 +37,10 @@ #include "PlayerBot/Base/PlayerbotMgr.h" #endif +#ifdef BUILD_VOICECHAT +#include "VoiceChat/VoiceChatMgr.h" +#endif + GroupMemberStatus GetGroupMemberStatus(const Player* member = nullptr) { uint8 flags = MEMBER_STATUS_OFFLINE; @@ -229,6 +233,13 @@ void Group::ConvertToRaid() for (member_citerator citr = m_memberSlots.begin(); citr != m_memberSlots.end(); ++citr) if (Player* player = sObjectMgr.GetPlayer(citr->guid)) player->UpdateForQuestWorldObjects(); + +#ifdef BUILD_VOICECHAT + if (!IsBattleGroup()) + { + sVoiceChatMgr.ConvertToRaidChannel(GetId()); + } +#endif } bool Group::AddInvite(Player* player) @@ -372,6 +383,27 @@ bool Group::AddMember(ObjectGuid guid, const char* name) WorldPacket groupDataPacket = groupData.BuildPacket(0, false); player->SendDirectMessage(groupDataPacket); } + +#ifdef BUILD_VOICECHAT + if (player->GetSession()->IsVoiceChatEnabled()) + { + if (!IsBattleGroup()) + { + if (IsRaidGroup()) + { + sVoiceChatMgr.AddToRaidVoiceChatChannel(guid, GetId()); + } + else + { + sVoiceChatMgr.AddToGroupVoiceChatChannel(guid, GetId()); + } + } + else + { + sVoiceChatMgr.AddToBattlegroundVoiceChatChannel(guid); + } + } +#endif } return true; @@ -449,6 +481,18 @@ uint32 Group::RemoveMember(ObjectGuid guid, uint8 method) }); SendUpdate(); + +#ifdef BUILD_VOICECHAT + if (!IsBattleGroup()) + { + sVoiceChatMgr.RemoveFromGroupVoiceChatChannel(guid, GetId()); + sVoiceChatMgr.RemoveFromRaidVoiceChatChannel(guid, GetId()); + } + else + { + sVoiceChatMgr.RemoveFromBattlegroundVoiceChatChannel(guid); + } +#endif } // if group before remove <= 2 disband it else @@ -536,6 +580,25 @@ void Group::Disband(bool hideDestroy) _updateLeaderFlag(true); m_leaderGuid.Clear(); m_leaderName.clear(); + +#ifdef BUILD_VOICECHAT + if (!IsBattleGroup()) + { + sVoiceChatMgr.DeleteGroupVoiceChatChannel(GetId()); + sVoiceChatMgr.DeleteRaidVoiceChatChannel(GetId()); + } + else + { + if (m_bgGroup->GetBgRaid(ALLIANCE) == this) + { + sVoiceChatMgr.DeleteBattlegroundVoiceChatChannel(m_bgGroup->GetInstanceId(), ALLIANCE); + } + else if (m_bgGroup->GetBgRaid(HORDE) == this) + { + sVoiceChatMgr.DeleteBattlegroundVoiceChatChannel(m_bgGroup->GetInstanceId(), HORDE); + } + } +#endif } void Group::SetTargetIcon(uint8 id, ObjectGuid targetGuid) diff --git a/src/game/Server/Opcodes.cpp b/src/game/Server/Opcodes.cpp index 119371d7f27..6dc13cb166f 100644 --- a/src/game/Server/Opcodes.cpp +++ b/src/game/Server/Opcodes.cpp @@ -997,25 +997,25 @@ OpcodeHandler opcodeTable[NUM_MSG_TYPES] = /*0x3C8*/ { "SMSG_FEATURE_SYSTEM_STATUS", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_ServerSide }, /*0x3C9*/ { "CMSG_GM_SHOW_COMPLAINTS", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, /*0x3CA*/ { "CMSG_GM_UNSQUELCH", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, - /*0x3CB*/ { "CMSG_CHANNEL_SILENCE_VOICE", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, + /*0x3CB*/ { "CMSG_CHANNEL_SILENCE_VOICE", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleChannelSilenceOpcode }, /*0x3CC*/ { "CMSG_CHANNEL_SILENCE_ALL", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, - /*0x3CD*/ { "CMSG_CHANNEL_UNSILENCE_VOICE", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, + /*0x3CD*/ { "CMSG_CHANNEL_UNSILENCE_VOICE", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleChannelUnsilenceOpcode }, /*0x3CE*/ { "CMSG_CHANNEL_UNSILENCE_ALL", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, /*0x3CF*/ { "CMSG_TARGET_CAST", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, /*0x3D0*/ { "CMSG_TARGET_SCRIPT_CAST", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, /*0x3D1*/ { "CMSG_CHANNEL_DISPLAY_LIST", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleChannelDisplayListQueryOpcode}, - /*0x3D2*/ { "CMSG_SET_ACTIVE_VOICE_CHANNEL", STATUS_AUTHED, PROCESS_THREADUNSAFE, &WorldSession::HandleSetActiveVoiceChannel }, + /*0x3D2*/ { "CMSG_SET_ACTIVE_VOICE_CHANNEL", STATUS_AUTHED, PROCESS_THREADUNSAFE, &WorldSession::HandleSetActiveVoiceChannelOpcode}, /*0x3D3*/ { "CMSG_GET_CHANNEL_MEMBER_COUNT", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleGetChannelMemberCountOpcode}, /*0x3D4*/ { "SMSG_CHANNEL_MEMBER_COUNT", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_ServerSide }, /*0x3D5*/ { "CMSG_CHANNEL_VOICE_ON", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleChannelVoiceOnOpcode }, - /*0x3D6*/ { "CMSG_CHANNEL_VOICE_OFF", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, + /*0x3D6*/ { "CMSG_CHANNEL_VOICE_OFF", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleChannelVoiceOffOpcode }, /*0x3D7*/ { "CMSG_DEBUG_LIST_TARGETS", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, /*0x3D8*/ { "SMSG_DEBUG_LIST_TARGETS", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_ServerSide }, /*0x3D9*/ { "SMSG_AVAILABLE_VOICE_CHANNEL", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_ServerSide }, - /*0x3DA*/ { "CMSG_ADD_VOICE_IGNORE", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, - /*0x3DB*/ { "CMSG_DEL_VOICE_IGNORE", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, - /*0x3DC*/ { "CMSG_PARTY_SILENCE", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, - /*0x3DD*/ { "CMSG_PARTY_UNSILENCE", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, + /*0x3DA*/ { "CMSG_ADD_VOICE_IGNORE", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleAddVoiceIgnoreOpcode }, + /*0x3DB*/ { "CMSG_DEL_VOICE_IGNORE", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleDelVoiceIgnoreOpcode }, + /*0x3DC*/ { "CMSG_PARTY_SILENCE", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandlePartySilenceOpcode }, + /*0x3DD*/ { "CMSG_PARTY_UNSILENCE", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandlePartyUnsilenceOpcode }, /*0x3DE*/ { "MSG_NOTIFY_PARTY_SQUELCH", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL }, /*0x3DF*/ { "SMSG_COMSAT_RECONNECT_TRY", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_ServerSide }, /*0x3E0*/ { "SMSG_COMSAT_DISCONNECT", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_ServerSide }, diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index eb8ecdf959f..7836dfa486e 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -46,6 +46,10 @@ #include #include +#ifdef BUILD_VOICECHAT +#include "VoiceChat/VoiceChatMgr.h" +#endif + #ifdef BUILD_DEPRECATED_PLAYERBOT #include "PlayerBot/Base/PlayerbotMgr.h" #include "PlayerBot/Base/PlayerbotAI.h" @@ -106,6 +110,9 @@ WorldSession::WorldSession(uint32 id, WorldSocket* sock, AccountTypes sec, uint8 m_latency(0), m_tutorialState(TUTORIALDATA_UNCHANGED), m_timeSyncClockDeltaQueue(6), m_timeSyncClockDelta(0), m_pendingTimeSyncRequests(), m_timeSyncNextCounter(0), m_timeSyncTimer(0), m_recruitingFriendId(recruitingFriend), m_isRecruiter(isARecruiter) +#ifdef BUILD_VOICECHAT + , m_voiceEnabled(false), m_micEnabled(false), m_currentVoiceChannel(0) +#endif {} /// WorldSession destructor @@ -133,6 +140,11 @@ void WorldSession::SetOffline() // friend status sSocialMgr.SendFriendStatus(_player, FRIEND_OFFLINE, _player->GetObjectGuid(), true); LogoutRequest(time(nullptr)); + +#ifdef BUILD_VOICECHAT + // Leave voice chat if player disconnects + sVoiceChatMgr.RemoveFromVoiceChatChannels(_player->GetObjectGuid()); +#endif } // be sure its closed (may occur when second session is opened) @@ -778,6 +790,11 @@ void WorldSession::LogoutPlayer() uint32 guid = _player->GetGUIDLow(); #endif +#ifdef BUILD_VOICECHAT + // Leave voice chat before player is removed + sVoiceChatMgr.RemoveFromVoiceChatChannels(_player->GetObjectGuid()); +#endif + ///- Remove the player from the world // the player may not be in the world when logging out // e.g if he got disconnected during a transfer to another map diff --git a/src/game/Server/WorldSession.h b/src/game/Server/WorldSession.h index 503ce3c9bc6..6fc8a470c30 100644 --- a/src/game/Server/WorldSession.h +++ b/src/game/Server/WorldSession.h @@ -449,6 +449,14 @@ class WorldSession uint32 GetRecruitingFriendId() const { return m_recruitingFriendId; } bool IsARecruiter() const { return m_isRecruiter; } +#ifdef BUILD_VOICECHAT + // Voice Chat + bool IsVoiceChatEnabled() const { return m_voiceEnabled; } + bool IsMicEnabled() const { return m_micEnabled; } + uint16 GetCurrentVoiceChannelId() const { return m_currentVoiceChannel; } + void SetCurrentVoiceChannelId(uint32 id) { m_currentVoiceChannel = id; } +#endif + // Time Synchronisation void ResetTimeSync(); void SendTimeSync(); @@ -543,6 +551,9 @@ class WorldSession void HandleDelFriendOpcode(WorldPacket& recvPacket); void HandleAddIgnoreOpcode(WorldPacket& recvPacket); static void HandleAddIgnoreOpcodeCallBack(QueryResult* result, uint32 accountId); +#ifdef BUILD_VOICECHAT + static void HandleAddMutedOpcodeCallBack(QueryResult* result, uint32 accountId); +#endif void HandleDelIgnoreOpcode(WorldPacket& recvPacket); void HandleSetContactNotesOpcode(WorldPacket& recvPacket); void HandleBugOpcode(WorldPacket& recvPacket); @@ -871,9 +882,32 @@ class WorldSession void HandleCancelTempEnchantmentOpcode(WorldPacket& recv_data); +#ifdef BUILD_VOICECHAT + // Voice Chat + void HandleAddVoiceIgnoreOpcode(WorldPacket& recvData); + void HandleDelVoiceIgnoreOpcode(WorldPacket& recvData); + void HandleChannelSilenceOpcode(WorldPacket& recvData); + void HandleChannelUnsilenceOpcode(WorldPacket& recvData); + void HandlePartySilenceOpcode(WorldPacket& recvData); + void HandlePartyUnsilenceOpcode(WorldPacket& recvData); void HandleChannelVoiceOnOpcode(WorldPacket& recv_data); + void HandleChannelVoiceOffOpcode(WorldPacket& recv_data); void HandleVoiceSessionEnableOpcode(WorldPacket& recv_data); - void HandleSetActiveVoiceChannel(WorldPacket& recv_data); + void HandleSetActiveVoiceChannelOpcode(WorldPacket& recv_data); +#else + // Voice Chat placeholder + void HandleAddVoiceIgnoreOpcode(WorldPacket& recvData) {} + void HandleDelVoiceIgnoreOpcode(WorldPacket& recvData) {} + void HandleChannelSilenceOpcode(WorldPacket& recvData) {} + void HandleChannelUnsilenceOpcode(WorldPacket& recvData) {} + void HandlePartySilenceOpcode(WorldPacket& recvData) {} + void HandlePartyUnsilenceOpcode(WorldPacket& recvData) {} + void HandleChannelVoiceOnOpcode(WorldPacket& recv_data) {} + void HandleChannelVoiceOffOpcode(WorldPacket& recv_data) {} + void HandleVoiceSessionEnableOpcode(WorldPacket& recv_data) {} + void HandleSetActiveVoiceChannelOpcode(WorldPacket& recv_data) {} +#endif + void HandleSetTaxiBenchmarkOpcode(WorldPacket& recv_data); void HandleCommentatorModeOpcode(WorldPacket& recv_data); @@ -993,6 +1027,13 @@ class WorldSession uint32 m_recruitingFriendId; bool m_isRecruiter; +#ifdef BUILD_VOICECHAT + // Voice Chat + bool m_micEnabled; + bool m_voiceEnabled; + uint16 m_currentVoiceChannel; +#endif + // Thread safety mechanisms std::mutex m_recvQueueLock; std::mutex m_recvQueueMapLock; diff --git a/src/game/Social/SocialMgr.cpp b/src/game/Social/SocialMgr.cpp index 343362e837f..42d2411443d 100644 --- a/src/game/Social/SocialMgr.cpp +++ b/src/game/Social/SocialMgr.cpp @@ -48,7 +48,11 @@ uint32 PlayerSocial::GetNumberOfSocialsWithFlag(SocialFlag flag) return counter; } +#ifdef BUILD_VOICECHAT +bool PlayerSocial::AddToSocialList(ObjectGuid friend_guid, bool ignore, bool muted) +#else bool PlayerSocial::AddToSocialList(ObjectGuid friend_guid, bool ignore) +#endif { // check client limits if (ignore) @@ -56,6 +60,13 @@ bool PlayerSocial::AddToSocialList(ObjectGuid friend_guid, bool ignore) if (GetNumberOfSocialsWithFlag(SOCIAL_FLAG_IGNORED) >= SOCIALMGR_IGNORE_LIMIT) return false; } +#ifdef BUILD_VOICECHAT + else if (muted) + { + if (GetNumberOfSocialsWithFlag(SOCIAL_FLAG_MUTED) >= SOCIALMGR_IGNORE_LIMIT) + return false; + } +#endif else { if (GetNumberOfSocialsWithFlag(SOCIAL_FLAG_FRIEND) >= SOCIALMGR_FRIEND_LIMIT) @@ -65,6 +76,10 @@ bool PlayerSocial::AddToSocialList(ObjectGuid friend_guid, bool ignore) uint32 flag = SOCIAL_FLAG_FRIEND; if (ignore) flag = SOCIAL_FLAG_IGNORED; +#ifdef BUILD_VOICECHAT + if (muted) + flag = SOCIAL_FLAG_MUTED; +#endif PlayerSocialMap::const_iterator itr = m_playerSocialMap.find(friend_guid.GetCounter()); if (itr != m_playerSocialMap.end()) @@ -82,7 +97,11 @@ bool PlayerSocial::AddToSocialList(ObjectGuid friend_guid, bool ignore) return true; } +#ifdef BUILD_VOICECHAT +void PlayerSocial::RemoveFromSocialList(ObjectGuid friend_guid, bool ignore, bool muted) +#else void PlayerSocial::RemoveFromSocialList(ObjectGuid friend_guid, bool ignore) +#endif { PlayerSocialMap::iterator itr = m_playerSocialMap.find(friend_guid.GetCounter()); if (itr == m_playerSocialMap.end()) // not exist @@ -91,6 +110,10 @@ void PlayerSocial::RemoveFromSocialList(ObjectGuid friend_guid, bool ignore) uint32 flag = SOCIAL_FLAG_FRIEND; if (ignore) flag = SOCIAL_FLAG_IGNORED; +#ifdef BUILD_VOICECHAT + if (muted) + flag = SOCIAL_FLAG_MUTED; +#endif itr->second.Flags &= ~flag; if (itr->second.Flags == 0) @@ -168,6 +191,14 @@ bool PlayerSocial::HasIgnore(ObjectGuid ignore_guid) return itr == m_playerSocialMap.end() ? false : (itr->second.Flags & SOCIAL_FLAG_IGNORED) != 0; } +#ifdef BUILD_VOICECHAT +bool PlayerSocial::HasMute(ObjectGuid ignore_guid) +{ + PlayerSocialMap::const_iterator itr = m_playerSocialMap.find(ignore_guid.GetCounter()); + return itr == m_playerSocialMap.end() ? false : (itr->second.Flags & SOCIAL_FLAG_MUTED) != 0; +} +#endif + SocialMgr::SocialMgr() { } @@ -303,6 +334,10 @@ PlayerSocial* SocialMgr::LoadFromDB(std::unique_ptr queryResult, Ob // used to speed up check below. Using GetNumberOfSocialsWithFlag will cause unneeded iteration uint32 friendCounter = 0, ignoreCounter = 0; +#ifdef BUILD_VOICECHAT + uint32 muteCounter = 0; +#endif + do { @@ -314,6 +349,10 @@ PlayerSocial* SocialMgr::LoadFromDB(std::unique_ptr queryResult, Ob if ((flags & SOCIAL_FLAG_IGNORED) && ignoreCounter >= SOCIALMGR_IGNORE_LIMIT) continue; +#ifdef BUILD_VOICECHAT + if ((flags & SOCIAL_FLAG_MUTED) && muteCounter >= SOCIALMGR_IGNORE_LIMIT) + continue; +#endif if ((flags & SOCIAL_FLAG_FRIEND) && friendCounter >= SOCIALMGR_FRIEND_LIMIT) continue; @@ -321,6 +360,10 @@ PlayerSocial* SocialMgr::LoadFromDB(std::unique_ptr queryResult, Ob if (flags & SOCIAL_FLAG_IGNORED) ++ignoreCounter; +#ifdef BUILD_VOICECHAT + else if (flags & SOCIAL_FLAG_MUTED) + ++muteCounter; +#endif else ++friendCounter; } diff --git a/src/game/Social/SocialMgr.h b/src/game/Social/SocialMgr.h index 7c71f90ed15..7092f406336 100644 --- a/src/game/Social/SocialMgr.h +++ b/src/game/Social/SocialMgr.h @@ -115,14 +115,22 @@ class PlayerSocial PlayerSocial(); ~PlayerSocial(); // adding/removing +#ifdef BUILD_VOICECHAT + bool AddToSocialList(ObjectGuid friend_guid, bool ignore, bool muted = false); + void RemoveFromSocialList(ObjectGuid friend_guid, bool ignore, bool muted = false); +#else bool AddToSocialList(ObjectGuid friend_guid, bool ignore); void RemoveFromSocialList(ObjectGuid friend_guid, bool ignore); +#endif void SetFriendNote(ObjectGuid friend_guid, std::string note); // Packet send's void SendSocialList(); // Misc bool HasFriend(ObjectGuid friend_guid); bool HasIgnore(ObjectGuid ignore_guid); +#ifdef BUILD_VOICECHAT + bool HasMute(ObjectGuid ignore_guid); +#endif void SetPlayerGuid(ObjectGuid guid) { m_playerLowGuid = guid.GetCounter(); } uint32 GetNumberOfSocialsWithFlag(SocialFlag flag); private: diff --git a/src/game/VoiceChat/VoiceChatChannel.cpp b/src/game/VoiceChat/VoiceChatChannel.cpp new file mode 100644 index 00000000000..880a2f6c762 --- /dev/null +++ b/src/game/VoiceChat/VoiceChatChannel.cpp @@ -0,0 +1,568 @@ +/* + * This file is part of the CMaNGOS Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "VoiceChatChannel.h" +#include "VoiceChatMgr.h" +#include "Chat/ChannelMgr.h" +#include "BattleGround/BattleGroundMgr.h" + +VoiceChatChannel::VoiceChatChannel(VoiceChatChannelTypes type, uint32 id, uint32 groupId, std::string name, Team team) +{ + m_channel_id = id; + m_type = type; + m_channel_name = name; + m_group_id = groupId; + m_team = team; + m_is_deleting = false; + m_is_mass_adding = false; + + for (uint8 i = 1; i < 250; ++i) + { + m_members_guids[i] = ObjectGuid(); + } + + GenerateSessionId(); + GenerateEncryptionKey(); +} +VoiceChatChannel::~VoiceChatChannel() +{ + m_is_deleting = true; + for (const auto & m_member : m_members) + { + RemoveVoiceChatMember(m_member.second.m_guid); + } +} + +void VoiceChatChannel::GenerateSessionId() +{ + // don't know what it should be so use time(nullptr) and increment to make it always different + m_session_id = sVoiceChatMgr.GetNewSessionId(); +} + +void VoiceChatChannel::GenerateEncryptionKey() +{ + // todo figure out encryption key + for (auto i = 0; i < 16; i++) + m_encryption_key[i] = 0x00; +} + +uint8 VoiceChatChannel::GenerateUserId(ObjectGuid guid) +{ + /*for (uint8 i = 1; i < 250; ++i) + { + if (m_members.find(i) == m_members.end()) + return i; + } + return 0;*/ + + // reusing user ID of a character that left voice chat makes other clients + // show that character name when new character is speaking. + // sending voice roster update doesn't seem to override it + // sending SMSG_VOICE_SESSION_LEAVE with player guid works but makes everyone leave current voice channel + // until fix is found just use new id for new characters + + // check if already has id + uint8 freeId = 0; + for (uint8 i = 1; i < 250; ++i) + { + if (m_members_guids[i] == guid) + return i; + + if (!m_members_guids[i]) + { + m_members_guids[i] = guid; + freeId = i; + break; + } + } + + return freeId; +} + +// let client know this voice channel is available +void VoiceChatChannel::SendAvailableVoiceChatChannel(WorldSession* session) +{ + Player* plr = session->GetPlayer(); + if (!plr) + return; + + sLog.outDebug("Sending SMSG_AVAILABLE_VOICE_CHANNEL for plr %u", plr->GetObjectGuid()); + + WorldPacket data(SMSG_AVAILABLE_VOICE_CHANNEL); + data << m_session_id; + data << uint8(m_type); + if (m_type == VOICECHAT_CHANNEL_CUSTOM) + data << m_channel_name; + else + data << uint8(0); + data << plr->GetObjectGuid(); + session->SendPacket(data); +} + +// send voice members list to all members +void VoiceChatChannel::SendVoiceRosterUpdate(bool empty, bool toAll, ObjectGuid toPlayer) +{ + // don't send while deleting channel and members + if (m_is_deleting) + return; + + // don't send while massively adding members after channel create + if (m_is_mass_adding) + return; + + WorldPacket data(SMSG_VOICE_SESSION_ROSTER_UPDATE, 31 + (m_members.size() * 11)); + data << uint64(m_session_id); + data << uint16(m_channel_id); + data << uint8(m_type); + if (m_type == VOICECHAT_CHANNEL_CUSTOM) + data << m_channel_name; + else + data << uint8(0); + data.append(m_encryption_key, 16); + data << uint32(htonl(sVoiceChatMgr.GetVoiceServerVoiceAddress())); + data << uint16(sVoiceChatMgr.GetVoiceServerVoicePort()); + + size_t count_pos = data.wpos(); + uint8 count = empty ? 1 : m_members.size(); + data << uint8(count); + + // send each member a list of that member plus others + size_t pos = data.wpos(); + for (const auto & m_member : m_members) + { + // clean up if disconnected + Player* plr = sObjectMgr.GetPlayer(m_member.second.m_guid, false); + if (!plr || plr->GetSession()->IsOffline()) + { + m_is_deleting = true; + + RemoveVoiceChatMember(m_member.second.m_guid); + if (!empty && count) + count--; + + m_is_deleting = false; + continue; + } + + data << m_member.second.m_guid; + data << m_member.second.user_id; + data << m_member.second.flags; + + for (const auto & j : m_members) + { + if (j.first == m_member.first) + continue; + + // clean up if disconnected + Player* other_plr = sObjectMgr.GetPlayer(j.second.m_guid, false); + if (!other_plr || other_plr->GetSession()->IsOffline()) + { + m_is_deleting = true; + + RemoveVoiceChatMember(j.second.m_guid); + if (!empty && count) + count--; + + m_is_deleting = false; + continue; + } + + data << j.second.m_guid; + data << j.second.user_id; + data << j.second.flags_unk; + data << j.second.flags; + } + + if (!empty) + data.put(count_pos, count); + + // do not send to not active users by default, don't know if it's okay + if ((!toPlayer && m_member.second.IsVoiced()) || toAll || plr->GetObjectGuid() == toPlayer) + plr->GetSession()->SendPacket(data); + + data.wpos(pos); + } +} + +// disable voice channel in client interface +void VoiceChatChannel::SendLeaveVoiceChatSession(WorldSession* session) const +{ + Player* plr = session->GetPlayer(); + if (!plr) + return; + + sLog.outDebug("Sending SMSG_VOICE_SESSION_LEAVE for plr %u", plr->GetObjectGuid()); + + WorldPacket data(SMSG_VOICE_SESSION_LEAVE, 16); + data << plr->GetObjectGuid(); + data << m_session_id; + session->SendPacket(data); +} + +// not used currently +void VoiceChatChannel::SendLeaveVoiceChatSession(ObjectGuid guid) +{ + for (auto& memb : m_members) + { + if (Player* plr = sObjectMgr.GetPlayer(memb.second.m_guid, false)) + { + if (WorldSession* session = plr->GetSession()) + { + WorldPacket data(SMSG_VOICE_SESSION_LEAVE, 16); + data << guid; + data << m_session_id; + session->SendPacket(data); + } + } + } +} + +void VoiceChatChannel::ConvertToRaid() +{ + if (GetType() == VOICECHAT_CHANNEL_RAID || GetType() != VOICECHAT_CHANNEL_GROUP) + return; + + SetType(VOICECHAT_CHANNEL_RAID); + SendVoiceRosterUpdate(false, true); +} + +VoiceChatMember* VoiceChatChannel::GetVoiceChatMember(ObjectGuid guid) +{ + for (auto& memb : m_members) + { + if (memb.second.m_guid == guid) + return &memb.second; + } + return nullptr; +} + +bool VoiceChatChannel::IsOn(ObjectGuid guid) +{ + for (VoiceChatMembers::const_iterator i = m_members.begin(); i != m_members.end(); ++i) + { + if (i->second.m_guid == guid) + return true; + } + return false; +} + +void VoiceChatChannel::AddVoiceChatMember(ObjectGuid guid) +{ + if (IsOn(guid)) + return; + + if (m_is_deleting) + return; + + Player* plr = sObjectMgr.GetPlayer(guid, false); + if (!plr) + { + sLog.outError("could not add voice member, player not found!"); + return; + } + + WorldSession* sess = plr->GetSession(); + if (!sess) + { + sLog.outError("could not add voice member, sess not found!"); + return; + } + + if (!sess->IsVoiceChatEnabled()) + { + sLog.outError("could not add voice member, voice chat disabled!"); + return; + } + + uint8 user_id = GenerateUserId(guid); + if (!user_id) + { + sLog.outError("could not add voice member, no free slots!"); + WorldPacket data(SMSG_VOICESESSION_FULL); + sess->SendPacket(data); + return; + } + + VoiceChatMember member = VoiceChatMember(guid, user_id); + member.SetEnabled(true); + member.SetVoiced(false); + member.SetMuted(!sess->IsMicEnabled()); + m_members[user_id] = member; + + // activate slot on voice server + sVoiceChatMgr.EnableChannelSlot(m_channel_id, user_id); + // notify voice server that mic is disabled + if (member.IsMuted()) + sVoiceChatMgr.MuteChannelSlot(m_channel_id, user_id); + + if (!m_is_mass_adding) + SendVoiceRosterUpdate(); + + SendAvailableVoiceChatChannel(sess); +} + +void VoiceChatChannel::RemoveVoiceChatMember(ObjectGuid guid) +{ + if (!IsOn(guid)) + return; + + VoiceChatMember* member = GetVoiceChatMember(guid); + if (!member) + { + sLog.outError("error in remove voice member, member not found!"); + return; + } + + Player* plr = sObjectMgr.GetPlayer(guid, false); + if (plr) + { + if (WorldSession* session = plr->GetSession()) + { + uint32 channelId = GetChannelId(); + session->GetMessager().AddMessage([channel = channelId](WorldSession* sess) + { + if (sess->GetCurrentVoiceChannelId() == channel) + sess->SetCurrentVoiceChannelId(0); + }); + + SendLeaveVoiceChatSession(session); + } + + //SendLeaveVoiceChatSession(guid); + } + + sVoiceChatMgr.DisableChannelSlot(m_channel_id, member->user_id); + + sLog.outDebug("user #%u removed from channel #%u", member->user_id, m_channel_id); + + m_members.erase(member->user_id); + + if (!m_members.empty()) + SendVoiceRosterUpdate(); +} + +void VoiceChatChannel::AddMembersAfterCreate() +{ + if (!sVoiceChatMgr.CanUseVoiceChat()) + return; + + // add all possible members to this channel + sLog.outDebug("VoiceChat: Adding voice chat enabled members after create, channel #%u, type #%u", m_channel_id, m_type); + + // disable sending voice roster update while adding + m_is_mass_adding = true; + + switch (GetType()) + { + case VOICECHAT_CHANNEL_GROUP: + case VOICECHAT_CHANNEL_RAID: + { + if (Group* group = sObjectMgr.GetGroupById(GetGroupId())) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + if (Player* groupMember = itr->getSource()) + { + if (WorldSession* session = groupMember->GetSession()) + { + if (session->IsVoiceChatEnabled()) + { + AddVoiceChatMember(groupMember->GetObjectGuid()); + } + } + } + } + } + break; + } + case VOICECHAT_CHANNEL_BG: + { + if (BattleGround* bg = sBattleGroundMgr.GetBattleGround(GetGroupId(), BATTLEGROUND_TYPE_NONE)) + { + for (const auto& itr : bg->GetPlayers()) + { + if (Player* bgMember = sObjectMgr.GetPlayer(itr.first, false)) + { + if (bgMember->GetTeam() != GetTeam()) + continue; + + if (WorldSession* session = bgMember->GetSession()) + { + if (session->IsVoiceChatEnabled()) + { + AddVoiceChatMember(itr.first); + } + } + } + } + } + break; + } + case VOICECHAT_CHANNEL_CUSTOM: + { + if (ChannelMgr* cMgr = channelMgr(GetTeam())) + { + Channel* chan = cMgr->GetChannel(GetChannelName(), nullptr, false); + if (chan) + { + chan->AddVoiceChatMembersAfterCreate(); + } + } + } + } + + // enable sending voice roster update + m_is_mass_adding = false; +} + +void VoiceChatChannel::VoiceMember(ObjectGuid guid) +{ + VoiceChatMember* member = GetVoiceChatMember(guid); + if (!member) + { + sLog.outError("error in voice voice member, member not found!"); + return; + } + + if (member->IsVoiced()) + return; + + member->SetVoiced(true); + + SendVoiceRosterUpdate(); + + sVoiceChatMgr.VoiceChannelSlot(m_channel_id, member->user_id); + + sLog.outDebug("user #%u voiced in channel #%u", member->user_id, m_channel_id); +} + +void VoiceChatChannel::DevoiceMember(ObjectGuid guid) +{ + VoiceChatMember* member = GetVoiceChatMember(guid); + if (!member) + { + sLog.outError("error in devoice voice member, member not found!"); + return; + } + + if (!member->IsVoiced()) + return; + + member->SetVoiced(false); + + // send roster to player + SendVoiceRosterUpdate(false, false, guid); + // send proper roster to other players + SendVoiceRosterUpdate(); + + sVoiceChatMgr.DevoiceChannelSlot(m_channel_id, member->user_id); + + sLog.outDebug("user #%u devoiced in channel #%u", member->user_id, m_channel_id); +} + +void VoiceChatChannel::MuteMember(ObjectGuid guid) +{ + VoiceChatMember* member = GetVoiceChatMember(guid); + if (!member) + { + sLog.outError("error in mute voice member, member not found!"); + return; + } + + if (member->IsMuted()) + return; + + member->SetMuted(true); + + SendVoiceRosterUpdate(); + + sVoiceChatMgr.MuteChannelSlot(m_channel_id, member->user_id); + + sLog.outDebug("user #%u muted in channel #%u", member->user_id, m_channel_id); +} + +void VoiceChatChannel::UnmuteMember(ObjectGuid guid) +{ + VoiceChatMember* member = GetVoiceChatMember(guid); + if (!member) + { + sLog.outError("error in unmute voice member, member not found!"); + return; + } + + if (!member->IsMuted()) + return; + + member->SetMuted(false); + + SendVoiceRosterUpdate(); + + // only allow leader to unmute force muted members + if (!member->IsForceMuted()) + sVoiceChatMgr.UnmuteChannelSlot(m_channel_id, member->user_id); + + sLog.outDebug("user #%u unmuted in channel #%u", member->user_id, m_channel_id); +} + +void VoiceChatChannel::ForceMuteMember(ObjectGuid guid) +{ + VoiceChatMember* member = GetVoiceChatMember(guid); + if (!member) + { + sLog.outError("error in force mute voice member, member not found!"); + return; + } + + if (member->IsForceMuted()) + return; + + member->SetForceMuted(true); + + SendVoiceRosterUpdate(); + + sVoiceChatMgr.MuteChannelSlot(m_channel_id, member->user_id); + + sLog.outDebug("user #%u force muted in channel #%u", member->user_id, m_channel_id); +} + +void VoiceChatChannel::ForceUnmuteMember(ObjectGuid guid) +{ + VoiceChatMember* member = GetVoiceChatMember(guid); + if (!member) + { + sLog.outError("error in force unmute voice member, member not found!"); + return; + } + + if (!member->IsForceMuted()) + return; + + bool micEnabled = !member->IsMuted(); + if (Player* plr = sObjectMgr.GetPlayer(guid, false)) + micEnabled = plr->GetSession()->IsMicEnabled(); + + member->SetForceMuted(false); + + SendVoiceRosterUpdate(); + + // do not let speak if mic disabled by client + if (micEnabled) + sVoiceChatMgr.UnmuteChannelSlot(m_channel_id, member->user_id); + + sLog.outDebug("user #%u force unmuted in channel #%u", member->user_id, m_channel_id); +} diff --git a/src/game/VoiceChat/VoiceChatChannel.h b/src/game/VoiceChat/VoiceChatChannel.h new file mode 100644 index 00000000000..8367fd425d1 --- /dev/null +++ b/src/game/VoiceChat/VoiceChatChannel.h @@ -0,0 +1,88 @@ +/* + * This file is part of the CMaNGOS Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _VOICECHATCHANNEL_H +#define _VOICECHATCHANNEL_H + +#include "VoiceChatDefines.h" + +struct VoiceChatMember; +typedef std::unordered_map VoiceChatMembers; +typedef std::unordered_map VoiceChatMembersGuids; + +class VoiceChatChannel +{ +public: + explicit VoiceChatChannel(VoiceChatChannelTypes type, uint32 id = 0, uint32 groupId = 0, std::string name = "", Team team = TEAM_BOTH_ALLOWED); + ~VoiceChatChannel(); + + void SetChannelId(uint16 id) { m_channel_id = id; } + uint16 GetChannelId() const { return m_channel_id; } + void SetType(VoiceChatChannelTypes type) { m_type = type; } + VoiceChatChannelTypes GetType() { return m_type; } + uint32 GetGroupId() const { return m_group_id; } + Team GetTeam() { return m_team; } + std::string GetChannelName() { return m_channel_name; } + + void GenerateSessionId(); + + void GenerateEncryptionKey(); + + uint8 GenerateUserId(ObjectGuid guid); + + uint64 GetSessionId() const { return m_session_id; } + + // send info to client + void SendAvailableVoiceChatChannel(WorldSession* session); + void SendVoiceRosterUpdate(bool empty = false, bool toAll = false, ObjectGuid toPlayer = ObjectGuid()); + void SendLeaveVoiceChatSession(WorldSession* session) const; + void SendLeaveVoiceChatSession(ObjectGuid guid); // not used currently + + void ConvertToRaid(); + + // manage users + VoiceChatMember* GetVoiceChatMember(ObjectGuid guid); + bool IsOn(ObjectGuid guid); + + void AddVoiceChatMember(ObjectGuid guid); + void RemoveVoiceChatMember(ObjectGuid guid); + void AddMembersAfterCreate(); + + // manage member state + void VoiceMember(ObjectGuid guid); + void DevoiceMember(ObjectGuid guid); + void MuteMember(ObjectGuid guid); + void UnmuteMember(ObjectGuid guid); + void ForceMuteMember(ObjectGuid guid); + void ForceUnmuteMember(ObjectGuid guid); + +private: + bool m_is_deleting; + bool m_is_mass_adding; + uint8 m_encryption_key[16]; + uint64 m_session_id; + uint16 m_channel_id; + VoiceChatChannelTypes m_type; + uint32 m_group_id; + std::string m_channel_name; + Team m_team; + VoiceChatMembers m_members; + VoiceChatMembersGuids m_members_guids; +}; + +#endif diff --git a/src/game/VoiceChat/VoiceChatDefines.h b/src/game/VoiceChat/VoiceChatDefines.h new file mode 100644 index 00000000000..874e869b03d --- /dev/null +++ b/src/game/VoiceChat/VoiceChatDefines.h @@ -0,0 +1,139 @@ +/* + * This file is part of the CMaNGOS Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _VOICECHATDEFINES_H +#define _VOICECHATDEFINES_H + +#include "Common.h" +#include "Globals/SharedDefines.h" +#include "Entities/ObjectGuid.h" + +enum VoiceChatServerOpcodes +{ + VOICECHAT_NULL_ACTION = 0, + VOICECHAT_CMSG_CREATE_CHANNEL = 1, + VOICECHAT_SMSG_CHANNEL_CREATED = 2, + VOICECHAT_CMSG_ADD_MEMBER = 3, + VOICECHAT_CMSG_REMOVE_MEMBER = 4, + VOICECHAT_CMSG_VOICE_MEMBER = 5, + VOICECHAT_CMSG_DEVOICE_MEMBER = 6, + VOICECHAT_CMSG_MUTE_MEMBER = 7, + VOICECHAT_CMSG_UNMUTE_MEMBER = 8, + VOICECHAT_CMSG_DELETE_CHANNEL = 9, + VOICECHAT_CMSG_PING = 10, + VOICECHAT_SMSG_PONG = 11, + VOICECHAT_NUM_OPCODES = 12, +}; + +struct VoiceChatChannelRequest +{ + uint32 id; + uint8 type; + uint32 groupid; + std::string channel_name; + Team team; +}; + +enum VoiceChatMemberFlags +{ + VOICECHAT_MEMBER_FLAG_ENABLED = 0x4, + VOICECHAT_MEMBER_FLAG_FORCE_MUTED = 0x8, + VOICECHAT_MEMBER_FLAG_MIC_MUTED = 0x20, + VOICECHAT_MEMBER_FLAG_VOICED = 0x40, +}; + +enum VoiceChatChannelTypes +{ + VOICECHAT_CHANNEL_CUSTOM = 0, + VOICECHAT_CHANNEL_BG = 1, + VOICECHAT_CHANNEL_GROUP = 2, + VOICECHAT_CHANNEL_RAID = 3, + VOICECHAT_CHANNEL_NONE = 4, +}; + +enum VoiceChatState +{ + VOICECHAT_DISCONNECTED = 0, + VOICECHAT_NOT_CONNECTED = 1, + VOICECHAT_CONNECTED = 2, + VOICECHAT_RECONNECTING = 3, +}; + +struct VoiceChatMember +{ + explicit VoiceChatMember(ObjectGuid guid = ObjectGuid(), uint8 userId = 0, uint8 userFlags = VOICECHAT_MEMBER_FLAG_ENABLED) + { + m_guid = guid; + user_id = userId; + flags = userFlags; + flags_unk = 0x80; + } + + ObjectGuid m_guid; + uint8 user_id; + uint8 flags; + uint8 flags_unk; + + inline bool HasFlag(uint8 flag) const { return (flags & flag) != 0; } + void SetFlag(uint8 flag, bool state) { if (state) flags |= flag; else flags &= ~flag; } + inline bool IsEnabled() const { return HasFlag(VOICECHAT_MEMBER_FLAG_ENABLED); } + inline void SetEnabled(bool state) { SetFlag(VOICECHAT_MEMBER_FLAG_ENABLED, state); } + inline bool IsVoiced() const { return HasFlag(VOICECHAT_MEMBER_FLAG_VOICED); } + inline void SetVoiced(bool state) { SetFlag(VOICECHAT_MEMBER_FLAG_VOICED, state); } + inline bool IsMuted() const { return HasFlag(VOICECHAT_MEMBER_FLAG_MIC_MUTED); } + inline void SetMuted(bool state) { SetFlag(VOICECHAT_MEMBER_FLAG_MIC_MUTED, state); } + inline bool IsForceMuted() const { return HasFlag(VOICECHAT_MEMBER_FLAG_FORCE_MUTED); } + inline void SetForceMuted(bool state) { SetFlag(VOICECHAT_MEMBER_FLAG_FORCE_MUTED, state); } +}; + +class VoiceChatServerPacket : public ByteBuffer +{ +public: + // just container for later use + VoiceChatServerPacket() : ByteBuffer(0), m_opcode(VOICECHAT_NULL_ACTION) + { + } + explicit VoiceChatServerPacket(VoiceChatServerOpcodes opcode, size_t res = 200) : ByteBuffer(res), m_opcode(opcode) { } + // copy constructor + VoiceChatServerPacket(const VoiceChatServerPacket& packet) : ByteBuffer(packet), m_opcode(packet.m_opcode) + { + } + + void Initialize(VoiceChatServerOpcodes opcode, size_t newres = 200) + { + clear(); + reserve(newres); + m_opcode = opcode; + } + + VoiceChatServerOpcodes GetOpcode() const { return m_opcode; } + void SetOpcode(VoiceChatServerOpcodes opcode) { m_opcode = opcode; } + +protected: + VoiceChatServerOpcodes m_opcode; +}; + +struct VoiceChatStatistics +{ + uint32 channels; + uint32 active_users; + uint32 totalVoiceChatEnabled; + uint32 totalVoiceMicEnabled; +}; + +#endif diff --git a/src/game/VoiceChat/VoiceChatHandler.cpp b/src/game/VoiceChat/VoiceChatHandler.cpp index 918446df834..48873008040 100644 --- a/src/game/VoiceChat/VoiceChatHandler.cpp +++ b/src/game/VoiceChat/VoiceChatHandler.cpp @@ -17,30 +17,504 @@ */ #include "Common.h" -#include "Server/WorldPacket.h" +#include "VoiceChatMgr.h" +#include "VoiceChatChannel.h" +#include "Chat/ChannelMgr.h" +#include "Social/SocialMgr.h" +#include "World/World.h" #include "Server/WorldSession.h" -#include "Log/Log.h" +#include "Tools/Language.h" -void WorldSession::HandleVoiceSessionEnableOpcode(WorldPacket& recv_data) +void WorldSession::HandleVoiceSessionEnableOpcode(WorldPacket & recv_data) { - DEBUG_LOG("WORLD: CMSG_VOICE_SESSION_ENABLE"); - // uint8 isVoiceEnabled, uint8 isMicrophoneEnabled - recv_data.read_skip(); - recv_data.read_skip(); - recv_data.hexlike(); + sLog.outDebug("WORLD: Received CMSG_VOICE_SESSION_ENABLE"); + + if (!sVoiceChatMgr.CanSeeVoiceChat()) + return; + + // comes from in game voice chat settings + // is sent during login or when changing settings + uint8 voiceEnabled, micEnabled; + recv_data >> voiceEnabled; + recv_data >> micEnabled; + + if(!voiceEnabled) + { + if (_player) + { + sVoiceChatMgr.RemoveFromVoiceChatChannels(_player->GetObjectGuid()); + SetCurrentVoiceChannelId(0); + } + } + else + { + // send available voice channels + if (_player && _player->IsInWorld() && !m_voiceEnabled) + { + // enable it here to allow joining channels + m_voiceEnabled = voiceEnabled; + m_micEnabled = micEnabled; + sVoiceChatMgr.JoinAvailableVoiceChatChannels(this); + } + } + + if (!micEnabled) + { + if (_player) + { + if (GetCurrentVoiceChannelId()) + { + VoiceChatChannel* current_channel = sVoiceChatMgr.GetVoiceChatChannel(GetCurrentVoiceChannelId()); + if (current_channel) + current_channel->MuteMember(_player->GetObjectGuid()); + } + } + } + else + { + if (_player) + { + if (GetCurrentVoiceChannelId()) + { + VoiceChatChannel* current_channel = sVoiceChatMgr.GetVoiceChatChannel(GetCurrentVoiceChannelId()); + if (current_channel) + current_channel->UnmuteMember(_player->GetObjectGuid()); + } + } + } + + m_micEnabled = micEnabled; + m_voiceEnabled = voiceEnabled; +} + +void WorldSession::HandleSetActiveVoiceChannelOpcode(WorldPacket & recv_data) +{ + sLog.outDebug("WORLD: Received CMSG_SET_ACTIVE_VOICE_CHANNEL"); + + if (!sVoiceChatMgr.CanUseVoiceChat()) + return; + + if (!_player || !_player->IsInWorld()) + return; + + uint32 type; + std::string name; + recv_data >> type; + + // leave current voice channel if player selects different one + VoiceChatChannel* current_channel = nullptr; + if (GetCurrentVoiceChannelId()) + current_channel = sVoiceChatMgr.GetVoiceChatChannel(GetCurrentVoiceChannelId()); + + switch (type) + { + case VOICECHAT_CHANNEL_CUSTOM: + { + recv_data >> name; + // custom channel + if (ChannelMgr* cMgr = channelMgr(_player->GetTeam())) + { + Channel* chan = cMgr->GetChannel(name, nullptr, false); + if (!chan || !chan->IsOn(_player->GetObjectGuid()) || chan->IsBanned(_player->GetObjectGuid()) || !chan->IsVoiceEnabled()) + return; + } + else + return; + + if (VoiceChatChannel* v_channel = sVoiceChatMgr.GetCustomVoiceChatChannel(name, _player->GetTeam())) + { + if (current_channel) + { + // if same channel, just update roster + if (v_channel == current_channel) + { + v_channel->SendVoiceRosterUpdate(); + return; + } + else + current_channel->DevoiceMember(_player->GetObjectGuid()); + } + + v_channel->AddVoiceChatMember(_player->GetObjectGuid()); + if (v_channel->IsOn(_player->GetObjectGuid())) + { + // change speaker icon from grey to color + v_channel->VoiceMember(_player->GetObjectGuid()); + // allow to speak depending on settings + if (IsMicEnabled()) + v_channel->UnmuteMember(_player->GetObjectGuid()); + else + v_channel->MuteMember(_player->GetObjectGuid()); + + SetCurrentVoiceChannelId(v_channel->GetChannelId()); + } + } + + break; + } + case VOICECHAT_CHANNEL_GROUP: + case VOICECHAT_CHANNEL_RAID: + { + // group + Group* grp = _player->GetGroup(); + if (grp && grp->IsBattleGroup()) + grp = _player->GetOriginalGroup(); + + if (grp) + { + VoiceChatChannel* v_channel = nullptr; + if (grp->IsRaidGroup()) + v_channel = sVoiceChatMgr.GetRaidVoiceChatChannel(grp->GetId()); + else + v_channel = sVoiceChatMgr.GetGroupVoiceChatChannel(grp->GetId()); + + if (v_channel) + { + if (current_channel) + { + // if same channel, just update roster + if (v_channel == current_channel) + { + v_channel->SendVoiceRosterUpdate(); + return; + } + else + current_channel->DevoiceMember(_player->GetObjectGuid()); + } + + v_channel->AddVoiceChatMember(_player->GetObjectGuid()); + if (v_channel->IsOn(_player->GetObjectGuid())) + { + // change speaker icon from grey to color + v_channel->VoiceMember(_player->GetObjectGuid()); + // allow to speak depending on settings + if (IsMicEnabled()) + v_channel->UnmuteMember(_player->GetObjectGuid()); + else + v_channel->MuteMember(_player->GetObjectGuid()); + + SetCurrentVoiceChannelId(v_channel->GetChannelId()); + } + } + } + + break; + } + case VOICECHAT_CHANNEL_BG: + { + // bg + if (_player->InBattleGround()) + { + VoiceChatChannel* v_channel = sVoiceChatMgr.GetBattlegroundVoiceChatChannel(_player->GetBattleGroundId(), _player->GetBGTeam()); + if (v_channel) + { + if (current_channel) + { + // if same channel, just update roster + if (v_channel == current_channel) + { + v_channel->SendVoiceRosterUpdate(); + return; + } + else + current_channel->DevoiceMember(_player->GetObjectGuid()); + } + + v_channel->AddVoiceChatMember(_player->GetObjectGuid()); + if (v_channel->IsOn(_player->GetObjectGuid())) + { + // change speaker icon from grey to color + v_channel->VoiceMember(_player->GetObjectGuid()); + // allow to speak depending on settings + if (IsMicEnabled()) + v_channel->UnmuteMember(_player->GetObjectGuid()); + else + v_channel->MuteMember(_player->GetObjectGuid()); + + SetCurrentVoiceChannelId(v_channel->GetChannelId()); + } + } + } + + break; + } + case VOICECHAT_CHANNEL_NONE: + { + // leave current channel + if (current_channel) + { + current_channel->DevoiceMember(_player->GetObjectGuid()); + } + SetCurrentVoiceChannelId(0); + + break; + } + default: + break; + } } void WorldSession::HandleChannelVoiceOnOpcode(WorldPacket& recv_data) { - DEBUG_LOG("WORLD: CMSG_CHANNEL_VOICE_ON"); - // Enable Voice button in channel context menu - recv_data.hexlike(); + sLog.outDebug("WORLD: Received CMSG_CHANNEL_VOICE_ON"); + + if (!sVoiceChatMgr.CanUseVoiceChat()) + return; + + std::string name; + recv_data >> name; + + if (!_player) + return; + + // custom channel + if (ChannelMgr* cMgr = channelMgr(_player->GetTeam())) + { + Channel* chn = cMgr->GetChannel(name, _player); + if (!chn) + return; + + if (chn->IsLFG() || chn->IsConstant()) + { + sLog.outError("VoiceChat: Channel is LFG or constant, can't use voice!"); + return; + } + + // already enabled + if (chn->IsVoiceEnabled()) + return; + + chn->ToggleVoice(_player); + + sVoiceChatMgr.CreateCustomVoiceChatChannel(chn->GetName(), _player->GetTeam()); + } +} + +void WorldSession::HandleChannelVoiceOffOpcode(WorldPacket& recv_data) +{ + sLog.outDebug("WORLD: Received CMSG_CHANNEL_VOICE_OFF"); + + // todo check if possible to send with chat commands +} + +void WorldSession::HandleAddVoiceIgnoreOpcode(WorldPacket& recvData) +{ + sLog.outDebug("WORLD: Received opcode CMSG_ADD_VOICE_IGNORE"); + + std::string IgnoreName = GetMangosString(LANG_FRIEND_IGNORE_UNKNOWN); + + recvData >> IgnoreName; + + if (!normalizePlayerName(IgnoreName)) + return; + + CharacterDatabase.escape_string(IgnoreName); + + DEBUG_LOG("WORLD: %s asked to Ignore: '%s'", + GetPlayer()->GetName(), IgnoreName.c_str()); + + CharacterDatabase.AsyncPQuery(&WorldSession::HandleAddMutedOpcodeCallBack, GetAccountId(), "SELECT guid FROM characters WHERE name = '%s'", IgnoreName.c_str()); } -void WorldSession::HandleSetActiveVoiceChannel(WorldPacket& recv_data) +void WorldSession::HandleAddMutedOpcodeCallBack(QueryResult* result, uint32 accountId) { - DEBUG_LOG("WORLD: CMSG_SET_ACTIVE_VOICE_CHANNEL"); - recv_data.read_skip(); - recv_data.read_skip(); - recv_data.hexlike(); + if (!result) + return; + + uint32 ignoreLowGuid = (*result)[0].GetUInt32(); + ObjectGuid ignoreGuid = ObjectGuid(HIGHGUID_PLAYER, ignoreLowGuid); + + delete result; + + WorldSession* session = sWorld.FindSession(accountId); + if (!session) + return; + + Player* player = session->GetPlayer(); + if (!player) + return; + + FriendsResult ignoreResult = FRIEND_MUTE_NOT_FOUND; + if (ignoreGuid) + { + if (ignoreGuid == player->GetObjectGuid()) + ignoreResult = FRIEND_MUTE_SELF; + else if (player->GetSocial()->HasMute(ignoreGuid)) + ignoreResult = FRIEND_MUTE_ALREADY; + else + { + ignoreResult = FRIEND_MUTE_ADDED; + + // mute list full + if (!player->GetSocial()->AddToSocialList(ignoreGuid, false, true)) + ignoreResult = FRIEND_MUTE_FULL; + } + } + + sSocialMgr.SendFriendStatus(player, ignoreResult, ignoreGuid, false); + + sLog.outDebug("WORLD: Sent (SMSG_FRIEND_STATUS)"); +} + +void WorldSession::HandleDelVoiceIgnoreOpcode(WorldPacket& recvData) +{ + ObjectGuid ignoreGuid; + + sLog.outDebug("WORLD: Received opcode CMSG_DEL_VOICE_IGNORE"); + + recvData >> ignoreGuid; + + _player->GetSocial()->RemoveFromSocialList(ignoreGuid, false, true); + + sSocialMgr.SendFriendStatus(GetPlayer(), FRIEND_MUTE_REMOVED, ignoreGuid, false); + + sLog.outDebug("WORLD: Sent motd (SMSG_FRIEND_STATUS)"); +} + +void WorldSession::HandlePartySilenceOpcode(WorldPacket& recvData) +{ + sLog.outDebug("WORLD: Received CMSG_PARTY_SILENCE"); + + if (!sVoiceChatMgr.CanUseVoiceChat()) + return; + + ObjectGuid ignoreGuid; + recvData >> ignoreGuid; + + if (!_player) + return; + + Group* grp = _player->GetGroup(); + if (!grp) + return; + + if (!grp->IsMember(ignoreGuid)) + return; + + if (!grp->IsLeader(GetPlayer()->GetObjectGuid()) && grp->GetMainAssistantGuid() != GetPlayer()->GetObjectGuid()) + return; + + VoiceChatChannel* v_channel = nullptr; + if (!grp->IsBattleGroup()) + { + if (grp->IsRaidGroup()) + v_channel = sVoiceChatMgr.GetRaidVoiceChatChannel(grp->GetId()); + else + v_channel = sVoiceChatMgr.GetGroupVoiceChatChannel(grp->GetId()); + } + else if (_player->InBattleGround()) + v_channel = sVoiceChatMgr.GetBattlegroundVoiceChatChannel(_player->GetBattleGroundId(), _player->GetBGTeam()); + + if (!v_channel) + return; + + v_channel->ForceMuteMember(ignoreGuid); +} + +void WorldSession::HandlePartyUnsilenceOpcode(WorldPacket& recvData) +{ + sLog.outDebug("WORLD: Received CMSG_PARTY_UNSILENCE"); + + if (!sVoiceChatMgr.CanUseVoiceChat()) + return; + + ObjectGuid ignoreGuid; + recvData >> ignoreGuid; + + if (!_player) + return; + + Group* grp = _player->GetGroup(); + if (!grp) + return; + + if (!grp->IsMember(ignoreGuid)) + return; + + if (!grp->IsLeader(GetPlayer()->GetObjectGuid()) && grp->GetMainAssistantGuid() != GetPlayer()->GetObjectGuid()) + return; + + VoiceChatChannel* v_channel = nullptr; + if (!grp->IsBattleGroup()) + { + if (grp->IsRaidGroup()) + v_channel = sVoiceChatMgr.GetRaidVoiceChatChannel(grp->GetId()); + else + v_channel = sVoiceChatMgr.GetGroupVoiceChatChannel(grp->GetId()); + } + else if (_player->InBattleGround()) + v_channel = sVoiceChatMgr.GetBattlegroundVoiceChatChannel(_player->GetBattleGroundId(), _player->GetBGTeam()); + + if (!v_channel) + return; + + v_channel->ForceUnmuteMember(ignoreGuid); +} + +void WorldSession::HandleChannelSilenceOpcode(WorldPacket& recvData) +{ + sLog.outDebug("WORLD: Received CMSG_CHANNEL_SILENCE"); + + if (!sVoiceChatMgr.CanUseVoiceChat()) + return; + + if (!_player) + return; + + std::string channelName, playerName; + recvData >> channelName >> playerName; + + if (ChannelMgr* cMgr = channelMgr(_player->GetTeam())) + { + Channel* chan = cMgr->GetChannel(channelName, nullptr, false); + if (chan && chan->IsVoiceEnabled()) + { + ObjectGuid plrGuid = sObjectMgr.GetPlayerGuidByName(playerName); + if (!plrGuid) + return; + + if (VoiceChatChannel* v_channel = sVoiceChatMgr.GetCustomVoiceChatChannel(channelName, _player->GetTeam())) + { + if (v_channel->IsOn(plrGuid)) + { + v_channel->ForceMuteMember(plrGuid); + chan->SetMicMute(_player, playerName.c_str(), true); + } + } + } + } +} + +void WorldSession::HandleChannelUnsilenceOpcode(WorldPacket& recvData) +{ + sLog.outDebug("WORLD: Received CMSG_CHANNEL_UNSILENCE"); + + if (!sVoiceChatMgr.CanUseVoiceChat()) + return; + + if (!_player) + return; + + std::string channelName, playerName; + recvData >> channelName >> playerName; + + if (ChannelMgr* cMgr = channelMgr(_player->GetTeam())) + { + Channel* chan = cMgr->GetChannel(channelName, nullptr, false); + if (chan && chan->IsVoiceEnabled()) + { + ObjectGuid plrGuid = sObjectMgr.GetPlayerGuidByName(playerName); + if (!plrGuid) + return; + + if (VoiceChatChannel* v_channel = sVoiceChatMgr.GetCustomVoiceChatChannel(channelName, _player->GetTeam())) + { + if (v_channel->IsOn(plrGuid)) + { + v_channel->ForceUnmuteMember(plrGuid); + chan->SetMicMute(_player, playerName.c_str(), false); + } + } + } + } } diff --git a/src/game/VoiceChat/VoiceChatMgr.cpp b/src/game/VoiceChat/VoiceChatMgr.cpp new file mode 100644 index 00000000000..f57ea73c78b --- /dev/null +++ b/src/game/VoiceChat/VoiceChatMgr.cpp @@ -0,0 +1,1126 @@ +/* + * This file is part of the CMaNGOS Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "VoiceChatMgr.h" +#include "VoiceChatChannel.h" +#include "Chat/ChannelMgr.h" +#include "BattleGround/BattleGroundMgr.h" +#include "World/World.h" +#include "Config/Config.h" + +#include "Network/AsyncConnector.hpp" +#include +#include + +void VoiceSocketThread() +{ + sVoiceChatMgr.VoiceSocketThread(); +} + +void ActivateVoiceSocketThread() +{ + boost::thread t(VoiceSocketThread); + t.detach(); +} + +INSTANTIATE_SINGLETON_1(VoiceChatMgr); + +VoiceChatMgr::VoiceChatMgr() +{ + enabled = false; + server_address = 0; + server_port = 0; + voice_port = 0; + maxConnectAttempts = 0; + new_request_id = 1; + new_session_id = time(nullptr);// 1; + curReconnectAttempts = 0; + m_socket = nullptr; + m_requestSocket = nullptr; + next_connect = time(nullptr); + next_ping = time(nullptr) + 5; + last_pong = time(nullptr); + lastUpdate = time(nullptr); + state = VOICECHAT_DISCONNECTED; +} + +void VoiceChatMgr::LoadConfigs() +{ + enabled = sConfig.GetBoolDefault("VoiceChat.Enabled", false); + + server_address_string = sConfig.GetStringDefault("VoiceChat.ServerAddress", "127.0.0.1"); + server_address = inet_addr(server_address_string.c_str()); + server_port = sConfig.GetIntDefault("VoiceChat.ServerPort", 3725); + + std::string voice_address_string = sConfig.GetStringDefault("VoiceChat.VoiceAddress", "127.0.0.1"); + voice_address = inet_addr(voice_address_string.c_str()); + voice_port = sConfig.GetIntDefault("VoiceChat.VoicePort", 3724); + + maxConnectAttempts = sConfig.GetIntDefault("VoiceChat.MaxConnectAttempts", -1); +} + +void VoiceChatMgr::Init() +{ + LoadConfigs(); + + next_ping = time(nullptr) + 5; + last_pong = time(nullptr); + lastUpdate = time(nullptr); + curReconnectAttempts = 0; + + state = enabled ? VOICECHAT_NOT_CONNECTED : VOICECHAT_DISCONNECTED; +} + +void VoiceChatMgr::Update() +{ + if (!enabled) + return; + + GetMessager().Execute(this); + + std::deque> recvQueueCopy; + { + std::lock_guard guard(m_recvQueueLock); + std::swap(recvQueueCopy, m_recvQueue); + } + + while (m_socket && !m_socket->IsClosed() && !recvQueueCopy.empty()) + { + auto const packet = std::move(recvQueueCopy.front()); + recvQueueCopy.pop_front(); + + try + { + HandleVoiceChatServerPacket(*packet); + } + catch (const ByteBufferException&) + { + ProcessByteBufferException(*packet); + } + } + + if (state == VOICECHAT_DISCONNECTED) + return; + + if (time(nullptr) - lastUpdate < 1) + return; + + lastUpdate = time(nullptr); + + if (m_requestSocket) + { + m_socket = m_requestSocket; + m_requestSocket = nullptr; + return; + } + + // connecting / reconnecting + if (!m_socket) + { + // lost connection + if (state == VOICECHAT_CONNECTED) + { + sLog.outError("VoiceChatMgr: Socket disconnected"); + SocketDisconnected(); + SendVoiceChatServiceDisconnect(); + state = VOICECHAT_RECONNECTING; + next_connect = time(nullptr) + 10; + return; + } + + // reconnecting + if (state == VOICECHAT_RECONNECTING) + { + // max attempts reached or disabled + if (maxConnectAttempts >= 0 && curReconnectAttempts >= maxConnectAttempts) + { + if (maxConnectAttempts > 0) + sLog.outError("VoiceChatMgr: Disconnected! Max reconnect attempts reached"); + else + sLog.outError("VoiceChatMgr: Disconnected! Reconnecting disabled"); + + DeleteAllChannels(); + SendVoiceChatStatus(false); + SendVoiceChatServiceConnectFail(); + curReconnectAttempts = 0; + state = VOICECHAT_DISCONNECTED; + return; + } + } + + // try to connect + if (time(nullptr) > next_connect) + { + if (NeedConnect() || NeedReconnect()) + { + ActivateVoiceSocketThread(); + } + + if (curReconnectAttempts > 0) + { + if (state == VOICECHAT_NOT_CONNECTED) + sLog.outError("VoiceChatMgr: Connect failed, will try again later"); + if (state == VOICECHAT_RECONNECTING) + sLog.outError("VoiceChatMgr: Reconnect failed, will try again later"); + } + + // count attempts + if (state == VOICECHAT_NOT_CONNECTED || state == VOICECHAT_RECONNECTING) + curReconnectAttempts++; + + // wait for socket to open + next_connect = time(nullptr) + 10; + return; + } + } + else + { + if (m_socket->IsClosed()) // lost connection + { + if (state == VOICECHAT_CONNECTED) + { + SocketDisconnected(); + SendVoiceChatServiceDisconnect(); + state = VOICECHAT_RECONNECTING; + return; + } + + // try to connect + if (time(nullptr) > next_connect) + { + if (!m_requestSocket && (state == VOICECHAT_NOT_CONNECTED || state == VOICECHAT_RECONNECTING)) + { + ActivateVoiceSocketThread(); + next_connect = time(nullptr) + 5; + } + } + return; + } + + // connecting / reconnecting + if (state == VOICECHAT_NOT_CONNECTED || state == VOICECHAT_RECONNECTING) + { + if (state == VOICECHAT_NOT_CONNECTED) + sLog.outBasic("VoiceChatMgr: Connected to %s:%u.", m_socket->GetRemoteAddress().c_str(), m_socket->GetRemotePort()); + if (state == VOICECHAT_RECONNECTING) + sLog.outBasic("VoiceChatMgr: Reconnected to %s:%u", m_socket->GetRemoteAddress().c_str(), m_socket->GetRemotePort()); + + SendVoiceChatStatus(true); + RestoreVoiceChatChannels(); + if (state == VOICECHAT_RECONNECTING) + { + SendVoiceChatServiceReconnected(); + } + + state = VOICECHAT_CONNECTED; + curReconnectAttempts = 0; + last_pong = time(nullptr); + } + else // connected + { + if (time(nullptr) >= next_ping) + { + next_ping = time(nullptr) + 5; + VoiceChatServerPacket data(VOICECHAT_CMSG_PING, 4); + data << uint32(0); + m_socket->SendPacket(data); + } + // because the above might kill m_socket + if (m_socket && (time(nullptr) - last_pong) > 10) + { + // ping timeout + sLog.outError("VoiceChatMgr: Ping timeout!"); + SocketDisconnected(); + SendVoiceChatServiceDisconnect(); + state = VOICECHAT_RECONNECTING; + next_connect = time(nullptr) + 10; + } + } + } +} + +void VoiceChatMgr::HandleVoiceChatServerPacket(VoiceChatServerPacket& pck) +{ + uint32 request_id; + uint8 error; + uint16 channel_id; + + switch(pck.GetOpcode()) + { + case VOICECHAT_SMSG_PONG: + { + last_pong = time(nullptr); + break; + } + case VOICECHAT_SMSG_CHANNEL_CREATED: + { + pck >> request_id; + pck >> error; + + for(auto request = m_requests.begin(); request != m_requests.end();) + { + if (request->id == request_id) + { + if (error == 0) + { + pck >> channel_id; + } + else + { + sLog.outError("VoiceChatMgr: Error creating voice channel"); + request = m_requests.erase(request); + return; + } + + if (request->groupid) + { + if (request->type == VOICECHAT_CHANNEL_GROUP || request->type == VOICECHAT_CHANNEL_RAID) + { + Group* grp = sObjectMgr.GetGroupById(request->groupid); + if (grp) + { + auto* v_channel = new VoiceChatChannel(VoiceChatChannelTypes(request->type), channel_id, grp->GetId()); + m_VoiceChatChannels.insert(std::make_pair((uint32)channel_id, v_channel)); + v_channel->AddMembersAfterCreate(); + } + } + else if (request->type == VOICECHAT_CHANNEL_BG) + { + BattleGround* bg = sBattleGroundMgr.GetBattleGround(request->groupid, BATTLEGROUND_TYPE_NONE); + if (bg) + { + // for BG use bg's instanceID as groupID + auto* v_channel = new VoiceChatChannel(VoiceChatChannelTypes(request->type), channel_id, request->groupid, "", request->team); + m_VoiceChatChannels.insert(std::make_pair((uint32)channel_id, v_channel)); + v_channel->AddMembersAfterCreate(); + } + } + } + else if (request->type == VOICECHAT_CHANNEL_CUSTOM) + { + if (ChannelMgr* cMgr = channelMgr(request->team)) + { + Channel* chan = cMgr->GetChannel(request->channel_name, nullptr, false); + if (chan) + { + auto* v_channel = new VoiceChatChannel(VoiceChatChannelTypes(request->type), channel_id, 0, request->channel_name, request->team); + m_VoiceChatChannels.insert(std::make_pair((uint32)channel_id, v_channel)); + v_channel->AddMembersAfterCreate(); + } + } + } + + request = m_requests.erase(request); + } + else + request++; + } + break; + } + default: + { + sLog.outError("VoiceChatMgr: received unknown opcode %u!\n", pck.GetOpcode()); + break; + } + } +} + +void VoiceChatMgr::SocketDisconnected() +{ + sLog.outBasic("VoiceChatMgr: VoiceChatServerSocket disconnected"); + + if (m_socket) + { + if (!m_socket->IsClosed()) + m_socket->Close(); + } + m_voiceService.stop(); + m_socket = nullptr; + m_requests.clear(); + + DeleteAllChannels(); + + curReconnectAttempts = 0; +} + +bool VoiceChatMgr::NeedConnect() +{ + return enabled && !m_socket && !m_requestSocket && state == VOICECHAT_NOT_CONNECTED && time(nullptr) > next_connect; +} + +bool VoiceChatMgr::NeedReconnect() +{ + return enabled && !m_socket && !m_requestSocket && state == VOICECHAT_RECONNECTING && time(nullptr) > next_connect; +} + +int32 VoiceChatMgr::GetReconnectAttempts() const +{ + if (maxConnectAttempts < 0 || (maxConnectAttempts > 0 && curReconnectAttempts < maxConnectAttempts)) + { + return maxConnectAttempts; + } + + return 0; +} + +bool VoiceChatMgr::RequestNewSocket(VoiceChatServerSocket* socket) +{ + if (m_requestSocket) + return false; + + m_requestSocket = socket->shared_from_this(); + return true; +} + +// Add an incoming packet to the queue +void VoiceChatMgr::QueuePacket(std::unique_ptr new_packet) +{ + std::lock_guard guard(m_recvQueueLock); + m_recvQueue.push_back(std::move(new_packet)); +} + +void VoiceChatMgr::ProcessByteBufferException(VoiceChatServerPacket const& packet) +{ + sLog.outError("VoiceChatMgr::Update ByteBufferException occured while parsing a packet (opcode: %u).", + packet.GetOpcode()); + + if (sLog.HasLogLevelOrHigher(LOG_LVL_DEBUG)) + { + DEBUG_LOG("Dumping error-causing voice server packet:"); + packet.hexlike(); + } + + DETAIL_LOG("Disconnecting voice server [address %s] for badly formatted packet.", + GetVoiceServerConnectAddressString().c_str()); + + GetMessager().AddMessage([](VoiceChatMgr* mgr) + { + mgr->SocketDisconnected(); + }); +} + +void VoiceChatMgr::VoiceSocketThread() +{ + m_voiceService.stop(); + m_voiceService.restart(); + std::unique_ptr> voiceSocket; + voiceSocket = std::make_unique>(m_voiceService, sVoiceChatMgr.GetVoiceServerConnectAddressString(), int32(sVoiceChatMgr.GetVoiceServerConnectPort()), false); + m_voiceService.run(); +} + +// enabled and connected to voice server +bool VoiceChatMgr::CanUseVoiceChat() +{ + return (enabled && m_socket); +} + +// enabled and is connected or trying to connect to voice server +bool VoiceChatMgr::CanSeeVoiceChat() +{ + return (enabled && state != VOICECHAT_DISCONNECTED); +} + +void VoiceChatMgr::CreateVoiceChatChannel(VoiceChatChannelTypes type, uint32 groupId, const std::string& name, Team team) +{ + if (!m_socket) + return; + + if (type == VOICECHAT_CHANNEL_NONE) + return; + + if (!groupId && name.empty()) + return; + + Team newTeam = GetCustomChannelTeam(team); + + if (IsVoiceChatChannelBeingCreated(type, groupId, name, newTeam)) + return; + + sLog.outDebug("VoiceChatMgr: CreateVoiceChannel type: %u, name: %s, team: %u, group: %u", type, name.c_str(), newTeam, groupId); + VoiceChatChannelRequest req; + req.id = new_request_id++; + req.type = type; + req.channel_name = name; + req.team = newTeam; + req.groupid = groupId; + m_requests.push_back(req); + + VoiceChatServerPacket data(VOICECHAT_CMSG_CREATE_CHANNEL, 5); + data << req.type; + data << req.id; + m_socket->SendPacket(data); +} + +void VoiceChatMgr::DeleteVoiceChatChannel(VoiceChatChannel* channel) +{ + if (!channel) + return; + + sLog.outDebug("VoiceChatMgr: DeleteVoiceChannel id: %u type: %u", channel->GetChannelId(), channel->GetType()); + + uint8 type = channel->GetType(); + uint16 id = channel->GetChannelId(); + + // disable voice in custom channel + if (type == VOICECHAT_CHANNEL_CUSTOM) + { + if (ChannelMgr* cMgr = channelMgr(channel->GetTeam())) + { + if (Channel* chn = cMgr->GetChannel(channel->GetChannelName(), nullptr, false)) + { + if (chn->IsVoiceEnabled()) + chn->ToggleVoice(); + } + } + } + + m_VoiceChatChannels.erase(channel->GetChannelId()); + delete channel; + + if (m_socket) + { + VoiceChatServerPacket data(VOICECHAT_CMSG_DELETE_CHANNEL, 5); + data << type; + data << id; + m_socket->SendPacket(data); + } +} + +// check if channel request has already been created +bool VoiceChatMgr::IsVoiceChatChannelBeingCreated(VoiceChatChannelTypes type, uint32 groupId, const std::string& name, Team team) +{ + for (const auto& req : m_requests) + { + if (req.type != type) + continue; + if (groupId && req.groupid != groupId) + continue; + if (!name.empty() && req.channel_name != name) + continue; + if (req.team != team) + continue; + + return true; + } + return false; +} + +void VoiceChatMgr::CreateGroupVoiceChatChannel(uint32 groupId) +{ + if (GetGroupVoiceChatChannel(groupId)) + return; + + CreateVoiceChatChannel(VOICECHAT_CHANNEL_GROUP, groupId); +} + +void VoiceChatMgr::CreateRaidVoiceChatChannel(uint32 groupId) +{ + if (GetRaidVoiceChatChannel(groupId)) + return; + + CreateVoiceChatChannel(VOICECHAT_CHANNEL_RAID, groupId); +} + +void VoiceChatMgr::CreateBattlegroundVoiceChatChannel(uint32 instanceId, Team team) +{ + if (GetBattlegroundVoiceChatChannel(instanceId, team)) + return; + + CreateVoiceChatChannel(VOICECHAT_CHANNEL_BG, instanceId, "", team); +} + +void VoiceChatMgr::CreateCustomVoiceChatChannel(const std::string& name, Team team) +{ + if (GetCustomVoiceChatChannel(name, team)) + return; + + CreateVoiceChatChannel(VOICECHAT_CHANNEL_CUSTOM, 0, name, team); +} + +void VoiceChatMgr::DeleteGroupVoiceChatChannel(uint32 groupId) +{ + if (!groupId) + return; + + if (VoiceChatChannel* channel = GetGroupVoiceChatChannel(groupId)) + { + DeleteVoiceChatChannel(channel); + } +} + +void VoiceChatMgr::DeleteRaidVoiceChatChannel(uint32 groupId) +{ + if (!groupId) + return; + + if (VoiceChatChannel* channel = GetRaidVoiceChatChannel(groupId)) + { + DeleteVoiceChatChannel(channel); + } +} + +void VoiceChatMgr::DeleteBattlegroundVoiceChatChannel(uint32 instanceId, Team team) +{ + if (!instanceId) + return; + + if (VoiceChatChannel* channel = GetBattlegroundVoiceChatChannel(instanceId, team)) + { + DeleteVoiceChatChannel(channel); + } +} + +void VoiceChatMgr::DeleteCustomVoiceChatChannel(const std::string& name, Team team) +{ + if (name.empty()) + return; + + if (VoiceChatChannel* v_channel = GetCustomVoiceChatChannel(name, team)) + { + DeleteVoiceChatChannel(v_channel); + } +} + +void VoiceChatMgr::ConvertToRaidChannel(uint32 groupId) +{ + if (VoiceChatChannel* chn = GetGroupVoiceChatChannel(groupId)) + { + chn->ConvertToRaid(); + } +} + +VoiceChatChannel* VoiceChatMgr::GetVoiceChatChannel(uint16 channel_id) +{ + auto itr = m_VoiceChatChannels.find(channel_id); + if (itr == m_VoiceChatChannels.end()) + return nullptr; + + return itr->second; +} + +VoiceChatChannel* VoiceChatMgr::GetGroupVoiceChatChannel(uint32 group_id) +{ + for (auto& channel : m_VoiceChatChannels) + { + VoiceChatChannel* chn = channel.second; + if (chn->GetType() == VOICECHAT_CHANNEL_GROUP && chn->GetGroupId() == group_id) + return chn; + } + + return nullptr; +} + +VoiceChatChannel* VoiceChatMgr::GetRaidVoiceChatChannel(uint32 group_id) +{ + for (auto& channel : m_VoiceChatChannels) + { + VoiceChatChannel* chn = channel.second; + if (chn->GetType() == VOICECHAT_CHANNEL_RAID && chn->GetGroupId() == group_id) + return chn; + } + + return nullptr; +} + +VoiceChatChannel* VoiceChatMgr::GetBattlegroundVoiceChatChannel(uint32 instanceId, Team team) +{ + for (auto& channel : m_VoiceChatChannels) + { + // for BG use bg's instanceID as groupID + VoiceChatChannel* chn = channel.second; + if (chn->GetType() == VOICECHAT_CHANNEL_BG && chn->GetGroupId() == instanceId && chn->GetTeam() == team) + return chn; + } + + return nullptr; +} + +VoiceChatChannel* VoiceChatMgr::GetCustomVoiceChatChannel(const std::string& name, Team team) +{ + for (auto& channels : m_VoiceChatChannels) + { + VoiceChatChannel* v_chan = channels.second; + if (v_chan->GetType() == VOICECHAT_CHANNEL_CUSTOM && v_chan->GetChannelName() == name && v_chan->GetTeam() == GetCustomChannelTeam(team)) + return v_chan; + } + + return nullptr; +} + +// get possible voice channels after login or voice chat enable +std::vector VoiceChatMgr::GetPossibleVoiceChatChannels(ObjectGuid guid) +{ + std::vector channel_list; + Player* plr = sObjectMgr.GetPlayer(guid, false); + if (!plr) + return channel_list; + + for (auto& channel : m_VoiceChatChannels) + { + VoiceChatChannel* chn = channel.second; + if (chn->GetType() != VOICECHAT_CHANNEL_CUSTOM) + continue; + + if (chn->GetTeam() != GetCustomChannelTeam(plr->GetTeam())) + continue; + + if (ChannelMgr* cMgr = channelMgr(plr->GetTeam())) + { + Channel* chan = cMgr->GetChannel(chn->GetChannelName(), nullptr, false); + if (chan && chan->IsOn(guid) && !chan->IsBanned(guid) && chan->IsVoiceEnabled()) + { + channel_list.push_back(chn); + } + } + } + + return channel_list; +} + +// create group/raid/bg channels after (re)connect to voice server +void VoiceChatMgr::RestoreVoiceChatChannels() +{ + sWorld.GetMessager().AddMessage([](World* world) + { + world->ExecuteForAllSessions([](auto& data) + { + const WorldSession& sess = data; + if (sess.IsVoiceChatEnabled()) + { + if (Player* plr = sess.GetPlayer()) + { + if (Group* grp = plr->GetGroup()) + { + if (!grp->IsBattleGroup()) + { + if (grp->IsRaidGroup()) + sVoiceChatMgr.AddToRaidVoiceChatChannel(plr->GetObjectGuid(), grp->GetId()); + else + sVoiceChatMgr.AddToGroupVoiceChatChannel(plr->GetObjectGuid(), grp->GetId()); + } + else + { + sVoiceChatMgr.AddToBattlegroundVoiceChatChannel(plr->GetObjectGuid()); + } + } + if (Group* grp = plr->GetOriginalGroup()) + { + if (!grp->IsBattleGroup()) + { + if (grp->IsRaidGroup()) + sVoiceChatMgr.AddToRaidVoiceChatChannel(plr->GetObjectGuid(), grp->GetId()); + else + sVoiceChatMgr.AddToGroupVoiceChatChannel(plr->GetObjectGuid(), grp->GetId()); + } + else + { + sVoiceChatMgr.AddToBattlegroundVoiceChatChannel(plr->GetObjectGuid()); + } + } + } + } + }); + }); +} + +void VoiceChatMgr::DeleteAllChannels() +{ + for (auto& channel : m_VoiceChatChannels) + { + DeleteVoiceChatChannel(channel.second); + } + m_VoiceChatChannels.clear(); +} + +// if cross faction channels are enabled, team is always ALLIANCE +Team VoiceChatMgr::GetCustomChannelTeam(Team team) +{ + if (sWorld.getConfig(CONFIG_BOOL_ALLOW_TWO_SIDE_INTERACTION_CHANNEL)) + return ALLIANCE; + else + return team; +} + +void VoiceChatMgr::AddToGroupVoiceChatChannel(ObjectGuid guid, uint32 groupId) +{ + if (!groupId) + return; + + VoiceChatChannel* v_channel = GetGroupVoiceChatChannel(groupId); + if (!v_channel) + { + CreateGroupVoiceChatChannel(groupId); + return; + } + + v_channel->AddVoiceChatMember(guid); +} + +void VoiceChatMgr::AddToRaidVoiceChatChannel(ObjectGuid guid, uint32 groupId) +{ + if (!groupId) + return; + + VoiceChatChannel* v_channel = GetRaidVoiceChatChannel(groupId); + if (!v_channel) + { + CreateRaidVoiceChatChannel(groupId); + return; + } + + v_channel->AddVoiceChatMember(guid); +} + +void VoiceChatMgr::AddToBattlegroundVoiceChatChannel(ObjectGuid guid) +{ + Player* plr = sObjectMgr.GetPlayer(guid); + if (!plr) + return; + + if (!plr->InBattleGround()) + return; + + // for BG use bg's instanceID as groupID + VoiceChatChannel* v_channel = GetBattlegroundVoiceChatChannel(plr->GetBattleGroundId(), plr->GetBGTeam()); + if (!v_channel) + { + CreateBattlegroundVoiceChatChannel(plr->GetBattleGroundId(), plr->GetBGTeam()); + return; + } + + v_channel->AddVoiceChatMember(guid); +} + +void VoiceChatMgr::AddToCustomVoiceChatChannel(ObjectGuid guid, const std::string& name, Team team) +{ + if (name.empty()) + return; + + VoiceChatChannel* v_channel = GetCustomVoiceChatChannel(name, team); + if (!v_channel) + { + CreateCustomVoiceChatChannel(name, team); + return; + } + + v_channel->AddVoiceChatMember(guid); +} + +void VoiceChatMgr::RemoveFromGroupVoiceChatChannel(ObjectGuid guid, uint32 groupId) +{ + if (!groupId) + return; + + if (VoiceChatChannel* v_channel = GetGroupVoiceChatChannel(groupId)) + { + v_channel->RemoveVoiceChatMember(guid); + } +} + +void VoiceChatMgr::RemoveFromRaidVoiceChatChannel(ObjectGuid guid, uint32 groupId) +{ + if (!groupId) + return; + + if (VoiceChatChannel* v_channel = GetRaidVoiceChatChannel(groupId)) + { + v_channel->RemoveVoiceChatMember(guid); + } +} + +void VoiceChatMgr::RemoveFromBattlegroundVoiceChatChannel(ObjectGuid guid) +{ + Player* plr = sObjectMgr.GetPlayer(guid); + if (!plr) + return; + + if (!plr->InBattleGround()) + return; + + if (VoiceChatChannel* v_channel = GetBattlegroundVoiceChatChannel(plr->GetBattleGroundId(), plr->GetBGTeam())) + { + v_channel->RemoveVoiceChatMember(guid); + } +} + +void VoiceChatMgr::RemoveFromCustomVoiceChatChannel(ObjectGuid guid, const std::string& name, Team team) +{ + if (name.empty()) + return; + + if (VoiceChatChannel* v_channel = GetCustomVoiceChatChannel(name, team)) + { + v_channel->RemoveVoiceChatMember(guid); + } +} + +void VoiceChatMgr::EnableChannelSlot(uint16 channel_id, uint8 slot_id) +{ + if(!m_socket) + return; + + sLog.outDebug("VoiceChatMgr: Channel %u activate slot %u", (int)channel_id, (int)slot_id); + + VoiceChatServerPacket data(VOICECHAT_CMSG_ADD_MEMBER, 5); + data << channel_id; + data << slot_id; + m_socket->SendPacket(data); +} + +void VoiceChatMgr::DisableChannelSlot(uint16 channel_id, uint8 slot_id) +{ + if(!m_socket) + return; + + sLog.outDebug("VoiceChatMgr: Channel %u deactivate slot %u", (int)channel_id, (int)slot_id); + + VoiceChatServerPacket data(VOICECHAT_CMSG_REMOVE_MEMBER, 5); + data << channel_id; + data << slot_id; + m_socket->SendPacket(data); +} + +void VoiceChatMgr::VoiceChannelSlot(uint16 channel_id, uint8 slot_id) +{ + if (!m_socket) + return; + + sLog.outDebug("VoiceChatMgr: Channel %u voice slot %u", (int)channel_id, (int)slot_id); + + VoiceChatServerPacket data(VOICECHAT_CMSG_VOICE_MEMBER, 5); + data << channel_id; + data << slot_id; + m_socket->SendPacket(data); +} + +void VoiceChatMgr::DevoiceChannelSlot(uint16 channel_id, uint8 slot_id) +{ + if (!m_socket) + return; + + sLog.outDebug("VoiceChatMgr: Channel %u devoice slot %u", (int)channel_id, (int)slot_id); + + VoiceChatServerPacket data(VOICECHAT_CMSG_DEVOICE_MEMBER, 5); + data << channel_id; + data << slot_id; + m_socket->SendPacket(data); +} + +void VoiceChatMgr::MuteChannelSlot(uint16 channel_id, uint8 slot_id) +{ + if (!m_socket) + return; + + sLog.outDebug("VoiceChatMgr: Channel %u mute slot %u", (int)channel_id, (int)slot_id); + + VoiceChatServerPacket data(VOICECHAT_CMSG_MUTE_MEMBER, 5); + data << channel_id; + data << slot_id; + m_socket->SendPacket(data); +} + +void VoiceChatMgr::UnmuteChannelSlot(uint16 channel_id, uint8 slot_id) +{ + if (!m_socket) + return; + + sLog.outDebug("VoiceChatMgr: Channel %u unmute slot %u", (int)channel_id, (int)slot_id); + + VoiceChatServerPacket data(VOICECHAT_CMSG_UNMUTE_MEMBER, 5); + data << channel_id; + data << slot_id; + m_socket->SendPacket(data); +} + +void VoiceChatMgr::JoinAvailableVoiceChatChannels(WorldSession* session) +{ + if (!CanUseVoiceChat()) + return; + + // send available voice channels + if (Player* player = session->GetPlayer()) + { + if (Group* grp = player->GetGroup()) + { + if (!grp->IsBattleGroup()) + { + if (grp->IsRaidGroup()) + AddToRaidVoiceChatChannel(player->GetObjectGuid(), grp->GetId()); + else + AddToGroupVoiceChatChannel(player->GetObjectGuid(), grp->GetId()); + } + else + { + AddToBattlegroundVoiceChatChannel(player->GetObjectGuid()); + } + } + if (Group* grp = player->GetOriginalGroup()) + { + if (grp->IsRaidGroup()) + AddToRaidVoiceChatChannel(player->GetObjectGuid(), grp->GetId()); + else + AddToGroupVoiceChatChannel(player->GetObjectGuid(), grp->GetId()); + } + + std::vector channel_list = GetPossibleVoiceChatChannels(player->GetObjectGuid()); + for (const auto& channel : channel_list) + { + channel->AddVoiceChatMember(player->GetObjectGuid()); + } + } +} + +// Not used currently +void VoiceChatMgr::SendAvailableVoiceChatChannels(WorldSession* session) +{ + if (!CanUseVoiceChat()) + return; + + // send available voice channels + if (Player* player = session->GetPlayer()) + { + if (Group* grp = player->GetGroup()) + { + if (!grp->IsBattleGroup()) + { + if (grp->IsRaidGroup()) + { + if (VoiceChatChannel* chn = GetRaidVoiceChatChannel(grp->GetId())) + { + chn->SendAvailableVoiceChatChannel(session); + } + } + else + { + if (VoiceChatChannel* chn = GetGroupVoiceChatChannel(grp->GetId())) + { + chn->SendAvailableVoiceChatChannel(session); + } + } + } + else + { + if (VoiceChatChannel* chn = GetBattlegroundVoiceChatChannel(player->GetBattleGroundId(), player->GetBGTeam())) + { + chn->SendAvailableVoiceChatChannel(session); + } + } + } + if (Group* grp = player->GetOriginalGroup()) + { + if (grp->IsRaidGroup()) + { + if (VoiceChatChannel* chn = GetRaidVoiceChatChannel(grp->GetId())) + { + chn->SendAvailableVoiceChatChannel(session); + } + } + else + { + if (VoiceChatChannel* chn = GetGroupVoiceChatChannel(grp->GetId())) + { + chn->SendAvailableVoiceChatChannel(session); + } + } + } + + std::vector channel_list = GetPossibleVoiceChatChannels(player->GetObjectGuid()); + for (auto channel : channel_list) + { + channel->SendAvailableVoiceChatChannel(session); + } + } +} + +void VoiceChatMgr::RemoveFromVoiceChatChannels(ObjectGuid guid) +{ + for (const auto& channel : m_VoiceChatChannels) + { + VoiceChatChannel* chn = channel.second; + chn->RemoveVoiceChatMember(guid); + } +} + +void VoiceChatMgr::SendVoiceChatStatus(bool status) +{ + WorldPacket data(SMSG_VOICE_CHAT_STATUS, 1); + data << uint8(status); + sWorld.SendGlobalMessage(data); +} + +void VoiceChatMgr::SendVoiceChatServiceMessage(Opcodes opcode) +{ + WorldPacket data(opcode); + sWorld.ExecuteForAllSessions([data](WorldSession& worldSession) + { + if (worldSession.IsVoiceChatEnabled()) + worldSession.SendPacket(data); + }); +} + +// command handlers + +void VoiceChatMgr::DisableVoiceChat() +{ + if (!m_voiceService.stopped() || (m_socket && !m_socket->IsClosed())) + { + SocketDisconnected(); + } + + enabled = false; + state = VOICECHAT_DISCONNECTED; + SendVoiceChatStatus(false); +} + +void VoiceChatMgr::EnableVoiceChat() +{ + if (enabled) + return; + + enabled = true; + Init(); + SendVoiceChatStatus(true); +} + +VoiceChatStatistics VoiceChatMgr::GetStatistics() +{ + VoiceChatStatistics stats; + + // amount of channels + stats.channels = 0; + for (const auto& chn : m_VoiceChatChannels) + { + stats.channels++; + } + + // amount of active users + stats.active_users = 0; + stats.totalVoiceChatEnabled = 0; + stats.totalVoiceMicEnabled = 0; + sWorld.ExecuteForAllSessions([&](WorldSession const& session) + { + if (session.IsVoiceChatEnabled()) + stats.totalVoiceChatEnabled++; + if (session.IsMicEnabled()) + stats.totalVoiceMicEnabled++; + if (session.GetCurrentVoiceChannelId()) + stats.active_users++; + }); + + return stats; +} diff --git a/src/game/VoiceChat/VoiceChatMgr.h b/src/game/VoiceChat/VoiceChatMgr.h new file mode 100644 index 00000000000..1a52e506205 --- /dev/null +++ b/src/game/VoiceChat/VoiceChatMgr.h @@ -0,0 +1,181 @@ +/* + * This file is part of the CMaNGOS Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _VOICECHATMGR_H +#define _VOICECHATMGR_H + +#include "VoiceChatServerSocket.h" +#include "VoiceChatDefines.h" +#include "Server/Opcodes.h" +class VoiceChatChannel; + +class VoiceChatMgr +{ +public: + + VoiceChatMgr(); + void Init(); + void LoadConfigs(); + void Update(); + void SocketDisconnected(); + bool RequestNewSocket(VoiceChatServerSocket* socket); + void QueuePacket(std::unique_ptr new_packet); + + void VoiceSocketThread(); + + bool NeedConnect(); + bool NeedReconnect(); + int32 GetReconnectAttempts() const; + + bool IsEnabled() const { return enabled; } + bool CanUseVoiceChat(); + bool CanSeeVoiceChat(); + + // configs + uint32 GetVoiceServerConnectAddress() const { return server_address; } + uint16 GetVoiceServerConnectPort() const { return server_port; } + uint32 GetVoiceServerVoiceAddress() const { return voice_address; } + uint16 GetVoiceServerVoicePort() const { return voice_port; } + std::string GetVoiceServerConnectAddressString() { return server_address_string; } + + // manage voice channels + void CreateVoiceChatChannel(VoiceChatChannelTypes type, uint32 groupId = 0, const std::string& name = "", Team team = TEAM_BOTH_ALLOWED); + void DeleteVoiceChatChannel(VoiceChatChannel* channel); + bool IsVoiceChatChannelBeingCreated(VoiceChatChannelTypes type, uint32 groupId = 0, const std::string& name = "", Team team = TEAM_BOTH_ALLOWED); + + void CreateGroupVoiceChatChannel(uint32 groupId); + void CreateRaidVoiceChatChannel(uint32 groupId); + void CreateBattlegroundVoiceChatChannel(uint32 instanceId, Team team); + void CreateCustomVoiceChatChannel(const std::string& name, Team team); + + void DeleteGroupVoiceChatChannel(uint32 groupId); + void DeleteRaidVoiceChatChannel(uint32 groupId); + void DeleteBattlegroundVoiceChatChannel(uint32 instanceId, Team team); + void DeleteCustomVoiceChatChannel(const std::string& name, Team team); + + void ConvertToRaidChannel(uint32 groupId); + + VoiceChatChannel* GetVoiceChatChannel(uint16 channel_id); + VoiceChatChannel* GetGroupVoiceChatChannel(uint32 group_id); + VoiceChatChannel* GetRaidVoiceChatChannel(uint32 group_id); + VoiceChatChannel* GetBattlegroundVoiceChatChannel(uint32 instanceId, Team team); + VoiceChatChannel* GetCustomVoiceChatChannel(const std::string& name, Team team); + std::vector GetPossibleVoiceChatChannels(ObjectGuid guid); + + // restore after reconnect + static void RestoreVoiceChatChannels(); + // delete after disconnect + void DeleteAllChannels(); + + // get proper team if cross faction channels enabled + static Team GetCustomChannelTeam(Team team); + + // manage users + void AddToGroupVoiceChatChannel(ObjectGuid guid, uint32 groupId); + void AddToRaidVoiceChatChannel(ObjectGuid guid, uint32 groupId); + void AddToBattlegroundVoiceChatChannel(ObjectGuid guid); + void AddToCustomVoiceChatChannel(ObjectGuid guid, const std::string& name, Team team); + + void RemoveFromGroupVoiceChatChannel(ObjectGuid guid, uint32 groupId); + void RemoveFromRaidVoiceChatChannel(ObjectGuid guid, uint32 groupId); + void RemoveFromBattlegroundVoiceChatChannel(ObjectGuid guid); + void RemoveFromCustomVoiceChatChannel(ObjectGuid guid, const std::string& name, Team team); + + // change user state on voice server + void EnableChannelSlot(uint16 channel_id, uint8 slot_id); + void DisableChannelSlot(uint16 channel_id, uint8 slot_id); + void VoiceChannelSlot(uint16 channel_id, uint8 slot_id); + void DevoiceChannelSlot(uint16 channel_id, uint8 slot_id); + void MuteChannelSlot(uint16 channel_id, uint8 slot_id); + void UnmuteChannelSlot(uint16 channel_id, uint8 slot_id); + + void JoinAvailableVoiceChatChannels(WorldSession* session); + void SendAvailableVoiceChatChannels(WorldSession* session); // Not used currently + + // remove from all channels + void RemoveFromVoiceChatChannels(ObjectGuid guid); + + uint64 GetNewSessionId() { return new_session_id++; } + + Messager& GetMessager() { return m_messager; } + + // Command Handlers + void DisableVoiceChat(); + void EnableVoiceChat(); + VoiceChatStatistics GetStatistics(); + +private: + + static void SendVoiceChatStatus(bool status); + static void SendVoiceChatServiceMessage(Opcodes opcode); + static void SendVoiceChatServiceDisconnect() { SendVoiceChatServiceMessage(SMSG_COMSAT_DISCONNECT); } + static void SendVoiceChatServiceConnectFail() { SendVoiceChatServiceMessage(SMSG_COMSAT_CONNECT_FAIL); } + static void SendVoiceChatServiceReconnected() { SendVoiceChatServiceMessage(SMSG_COMSAT_RECONNECT_TRY); } + + void HandleVoiceChatServerPacket(VoiceChatServerPacket& pck); + void ProcessByteBufferException(VoiceChatServerPacket const& packet); + + // socket to voice server + std::shared_ptr m_socket; + std::shared_ptr m_requestSocket; + std::vector m_requests; + uint32 new_request_id; + uint64 new_session_id; + + // configs + uint32 server_address; + uint16 server_port; + std::string server_address_string; + + // voice server address and udp port for client + uint32 voice_address; + uint16 voice_port; + + // next connect attempt + time_t next_connect; + time_t next_ping; + time_t last_pong; + + // enabled in config + bool enabled; + + // how many attemps to reconnect + int8 maxConnectAttempts; + // how many reconnect attempts have been made + uint8 curReconnectAttempts; + + // voice channels + std::map m_VoiceChatChannels; + + // state of connection + VoiceChatState state; + + uint32 lastUpdate; + + // Thread safety mechanisms + std::mutex m_recvQueueLock; + std::deque> m_recvQueue; + + Messager m_messager; + + boost::asio::io_service m_voiceService; +}; + +#define sVoiceChatMgr MaNGOS::Singleton::Instance() + +#endif diff --git a/src/game/VoiceChat/VoiceChatServerSocket.cpp b/src/game/VoiceChat/VoiceChatServerSocket.cpp new file mode 100644 index 00000000000..5ae0c0cb941 --- /dev/null +++ b/src/game/VoiceChat/VoiceChatServerSocket.cpp @@ -0,0 +1,140 @@ +/* + * This file is part of the CMaNGOS Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "VoiceChatServerSocket.h" +#include "Network/AsyncConnector.hpp" +#include "VoiceChatMgr.h" + +#include +#include + +#if defined( __GNUC__ ) +#pragma pack(1) +#else +#pragma pack(push,1) +#endif +struct VoiceChatServerPktHeader +{ + uint16 cmd; + uint16 size; + + const char* data() const { + return reinterpret_cast(this); + } + + std::size_t headerSize() const { + return sizeof(VoiceChatServerPktHeader); + } +}; +#if defined( __GNUC__ ) +#pragma pack() +#else +#pragma pack(pop) +#endif + +VoiceChatServerSocket::VoiceChatServerSocket(boost::asio::io_service& service) : AsyncSocket(service) +{} + +bool VoiceChatServerSocket::OnOpen() +{ + sVoiceChatMgr.RequestNewSocket(this->shared_from_this().get()); + return true; +} + +bool VoiceChatServerSocket::ProcessIncomingData() +{ + std::shared_ptr header = std::make_shared(); + + auto self(shared_from_this()); + + Read((char*)header.get(), sizeof(VoiceChatServerPktHeader), [self, header](const boost::system::error_code& error, std::size_t read) -> void + { + if (error) return; + + if ((header->size < 2) || (header->size > 0x2800) || (header->cmd >= VOICECHAT_NUM_OPCODES)) + { + sLog.outError("VoiceChatServerSocket::ProcessIncomingData: client sent malformed packet size = %u , cmd = %u", header->size, header->cmd); + return; + } + + const VoiceChatServerOpcodes opcode = static_cast(header->cmd); + + size_t packetSize = header->size; + std::shared_ptr> packetBuffer = std::make_shared>(packetSize); + + self->Read(reinterpret_cast(packetBuffer->data()), packetBuffer->size(), [self, packetBuffer, opcode = opcode](const boost::system::error_code& error, std::size_t read) -> void + { + if (error) return; + std::unique_ptr pct = std::make_unique(opcode, packetBuffer->size()); + pct->append(*packetBuffer.get()); + + try + { + sVoiceChatMgr.QueuePacket(std::move(pct)); + } + catch (ByteBufferException&) + { + sLog.outError("VoiceChatServerSocket::ProcessIncomingData ByteBufferException occured while parsing an instant handled packet (opcode: %u).", + opcode); + + if (sLog.HasLogLevelOrHigher(LOG_LVL_DEBUG)) + { + DEBUG_LOG("Dumping error-causing voice server packet:"); + pct->hexlike(); + } + + DETAIL_LOG("Disconnecting voice server [address %s] for badly formatted packet.", + self->GetRemoteAddress().c_str()); + + return; + } + self->ProcessIncomingData(); + }); + }); + + return true; +} + +void VoiceChatServerSocket::SendPacket(const VoiceChatServerPacket& pct) +{ + if (IsClosed()) + return; + + std::lock_guard guard(m_voiceChatServerSocketMutex); + + VoiceChatServerPktHeader header; + + header.cmd = pct.GetOpcode(); + header.size = static_cast(pct.size()); + + if (pct.size() > 0) + { + // allocate array for full message + std::shared_ptr> fullMessage = std::make_shared>(header.headerSize() + pct.size()); + std::memcpy(fullMessage->data(), header.data(), header.headerSize()); // copy header + std::memcpy((fullMessage->data() + header.headerSize()), reinterpret_cast(pct.contents()), pct.size()); // copy packet + auto self(shared_from_this()); + Write(fullMessage->data(), fullMessage->size(), [self, fullMessage](const boost::system::error_code& error, std::size_t read) {}); + } + else + { + std::shared_ptr sharedHeader = std::make_shared(header); + auto self(shared_from_this()); + Write(sharedHeader->data(), sharedHeader->headerSize(), [self, sharedHeader](const boost::system::error_code& error, std::size_t read) {}); + } +} diff --git a/src/game/VoiceChat/VoiceChatServerSocket.h b/src/game/VoiceChat/VoiceChatServerSocket.h new file mode 100644 index 00000000000..90a6fa1f1c6 --- /dev/null +++ b/src/game/VoiceChat/VoiceChatServerSocket.h @@ -0,0 +1,43 @@ +/* + * This file is part of the CMaNGOS Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _VOICECHATSERVERSSOCKET_H +#define _VOICECHATSERVERSSOCKET_H + +#include "Common.h" +#include "Network/AsyncSocket.hpp" + +#include +#include +#include + +class VoiceChatServerPacket; + +class VoiceChatServerSocket : public MaNGOS::AsyncSocket +{ + private: + bool ProcessIncomingData() override; + std::mutex m_voiceChatServerSocketMutex; + + public: + VoiceChatServerSocket(boost::asio::io_service& service); + bool OnOpen() override; + void SendPacket(const VoiceChatServerPacket& pct); +}; + +#endif diff --git a/src/game/World/World.cpp b/src/game/World/World.cpp index 2ef3d789142..14d3597c27c 100644 --- a/src/game/World/World.cpp +++ b/src/game/World/World.cpp @@ -72,6 +72,10 @@ #include "AuctionHouseBot/AuctionHouseBot.h" #endif +#ifdef BUILD_VOICECHAT +#include "VoiceChat/VoiceChatMgr.h" +#endif + #ifdef BUILD_METRICS #include "Metric/Metric.h" #endif @@ -166,6 +170,10 @@ World::~World() /// Cleanups before world stop void World::CleanupsBeforeStop() { +#ifdef BUILD_VOICECHAT + if (sVoiceChatMgr.CanUseVoiceChat()) + sVoiceChatMgr.SocketDisconnected(); // close voice socket and remove channels +#endif #ifdef ENABLE_PLAYERBOTS sRandomPlayerbotMgr.LogoutAllBots(); #endif @@ -1492,6 +1500,10 @@ void World::SetInitialWorldSettings() uint32 uStartInterval = WorldTimer::getMSTimeDiff(startTime, WorldTimer::getMSTime()); sLog.outString("SERVER STARTUP TIME: %i minutes %i seconds", uStartInterval / 60000, (uStartInterval % 60000) / 1000); sLog.outString(); + +#ifdef BUILD_VOICECHAT + sVoiceChatMgr.Init(); +#endif } void World::DetectDBCLang() @@ -1731,6 +1743,11 @@ void World::Update(uint32 diff) // cleanup unused GridMap objects as well as VMaps sTerrainMgr.Update(diff); + +#ifdef BUILD_VOICECHAT + sVoiceChatMgr.Update(); +#endif + #ifdef BUILD_METRICS auto updateEndTime = std::chrono::time_point_cast(Clock::now()); long long total = (updateEndTime - m_currentTime).count(); diff --git a/src/mangosd/CMakeLists.txt b/src/mangosd/CMakeLists.txt index 80febb8ea0b..f9cd8a67ffe 100644 --- a/src/mangosd/CMakeLists.txt +++ b/src/mangosd/CMakeLists.txt @@ -141,6 +141,11 @@ if (BUILD_PLAYERBOTS) add_definitions(-DENABLE_PLAYERBOTS) endif() +# Define BUILD_VOICECHAT if need +if (BUILD_VOICECHAT) + add_definitions(-DBUILD_VOICECHAT) +endif() + if(MSVC) install(FILES $ DESTINATION ${BIN_DIR} CONFIGURATIONS Debug) endif() diff --git a/src/mangosd/mangosd.conf.dist.in b/src/mangosd/mangosd.conf.dist.in index afe94ce9b2d..34826390980 100644 --- a/src/mangosd/mangosd.conf.dist.in +++ b/src/mangosd/mangosd.conf.dist.in @@ -1912,5 +1912,44 @@ Metric.Database = "perfd" Metric.Username = "" Metric.Password = "" +################################################################################################################### +# VOICE CHAT CONFIG +# +# VoiceChat.Enabled +# Enable Voice Chat +# Default: 0 - off +# 1 - on +# +# VoiceChat.ServerPort +# TCP port that world server should use to connect to voice chat server +# Default: 3725 +# +# VoiceChat.ServerAddress +# IP that world server should use to connect to voice chat server +# Default: 127.0.0.1 +# +# VoiceChat.VoicePort +# UDP port that game client should use to connect to voice chat server, should be available for client +# Default: 3724 +# +# VoiceChat.VoiceAddress +# IP that game client should use to connect to voice chat server, should be available for client +# Default: 127.0.0.1 +# +# VoiceChat.MaxConnectAttempts +# Maximum attempts to connect/reconnect to voice chat server +# Default: -1 (infinite) +# 0 (disabled) +# 1+ (limited attempts) +# +################################################################################################################### + +VoiceChat.Enabled = 0 +VoiceChat.ServerPort = 3725 +VoiceChat.ServerAddress = 127.0.0.1 +VoiceChat.VoicePort = 3724 +VoiceChat.VoiceAddress = 127.0.0.1 +VoiceChat.MaxConnectAttempts = -1 + Dummy.Debug1 = 0 Dummy.Debug2 = 0 diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt index 2a96a2b40f0..af4255851c8 100644 --- a/src/shared/CMakeLists.txt +++ b/src/shared/CMakeLists.txt @@ -99,6 +99,7 @@ endif() set(SRC_GRP_NETWORK Network/AsyncSocket.hpp Network/AsyncListener.hpp + Network/AsyncConnector.hpp ) set(SRC_GRP_PLATFORM diff --git a/src/shared/Network/AsyncConnector.hpp b/src/shared/Network/AsyncConnector.hpp new file mode 100644 index 00000000000..69c5aa3c795 --- /dev/null +++ b/src/shared/Network/AsyncConnector.hpp @@ -0,0 +1,67 @@ +/* +* This file is part of the CMaNGOS Project. See AUTHORS file for Copyright information +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +#ifndef MANGOSSERVER_ASYNC_CONNECTOR +#define MANGOSSERVER_ASYNC_CONNECTOR + +#include "Platform/Define.h" +#include +#include +#include "AsyncSocket.hpp" + +namespace MaNGOS +{ + template + class AsyncConnector + { + public: + AsyncConnector(boost::asio::io_service& io_service, std::string const& connectIp, unsigned short connectPort, bool silent = false) : m_service(io_service), m_endpoint(boost::asio::ip::address::from_string(connectIp), connectPort), m_silent(silent) + { + Connect(); + } + void HandleConnect(std::shared_ptr connection, const boost::system::error_code& err) + { + if (!err) + connection->Start(); + } + private: + boost::asio::io_service& m_service; + boost::asio::ip::tcp::endpoint m_endpoint; + bool m_silent; + void Connect() + { + // socket + std::shared_ptr connection = std::make_shared(m_service); + boost::asio::ip::tcp::socket& bSock = connection->GetAsioSocket(); + + try + { + bSock.async_connect(m_endpoint, boost::bind(&AsyncConnector::HandleConnect, this, connection, boost::asio::placeholders::error)); + } + catch (boost::system::system_error& error) + { + if (!m_silent) + { + sLog.outError("AsyncConnector:: failed to connect to remote address. Error: %s", error.what()); + } + } + } + }; +} + +#endif