//----------------------------------------------------------------------------- // File: XBVoice.cpp // // Stolen from June XDK SimpleVoice sample, then butchered // //----------------------------------------------------------------------------- #include "XBVoice.h" #include "XBLive.h" #include "..\client\client.h" #include "../win32/snd_fx_img.h" #include "../qcommon/qcommon.h" #include "../cgame/cg_local.h" #include "../qcommon/xb_settings.h" #define _UI // I'm going to hell. #include "..\ui\ui_shared.h" #undef _UI #ifndef FINAL_BUILD #include "../renderer/tr_font.h" #endif // The voice system: CVoiceManager g_Voice; // Port 1000 gives 0 extra port overhead on the wire - this is used the game // Ports 1001-1255 give 2 bytes overhead on the wire const WORD DIRECT_PORT = 1001; // I wish VDP and UDP were really different const WORD RELIABLE_PORT = 1002; // Port for low-bandwidth reliable msgs // Define some preset voice mask configurations struct VOICE_MASK_PRESET { char *strLabel; XHV_VOICE_MASK mask; }; const VOICE_MASK_PRESET g_VoiceMasks[] = { { "None", XHV_VOICE_MASK_NONE }, { "Anonymous", XHV_VOICE_MASK_ANONYMOUS }, }; const DWORD NUM_VOICEMASKS = sizeof( g_VoiceMasks ) / sizeof( g_VoiceMasks[0] ); // Maximum number of voice streams to use for playback const DWORD NUM_XHV_PLAYBACK_STREAMS = 2; // Small struct used in the code to determine who gets our voice struct VoiceReceiver { int index; // Client's index (into xbPlayerList) unsigned long dist; // Distance to this client in worldspace }; // We keep track of how many people (and who) can hear us static int numVoiceTargets = 0; static VoiceReceiver voiceTargets[MAX_ONLINE_PLAYERS]; // Non-constant for now, so we can test it: static int maxVoiceTargets = 5; //----------------------------------------------------------------------------- // Name: CVoiceManager() // Desc: Constructor //----------------------------------------------------------------------------- CVoiceManager::CVoiceManager() : m_DirectSock (), m_bInitialized ( false ), m_msgVoiceData (), m_VoiceTimer ( 0 ), m_bRunning ( false ), m_MuteListSize ( 0 ), m_MuteListTask ( NULL ), m_MuteState ( MUTE_IDLE ), m_VoiceMask ( 0 ), m_Channel ( CHAN_PRIMARY ), m_bVoiceDisabled ( false ) { // Nothing } //----------------------------------------------------------------------------- // Name: Initialize() // Desc: Initialize device-dependant objects //----------------------------------------------------------------------------- HRESULT CVoiceManager::Initialize() { // Don't do anything if we're already initialized (servers can do this) if( m_bInitialized ) return S_OK; extern LPDSEFFECTIMAGEDESC getEffectsImageDesc(void); // Set up parameters for the Voice Chat engine XHV_RUNTIME_PARAMS xhvParams; xhvParams.dwMaxLocalTalkers = 4; // XGetPortCount(); xhvParams.dwMaxRemoteTalkers = MAX_ONLINE_PLAYERS - 1; // One per box #ifdef _DEBUG xhvParams.dwMaxCompressedBuffers = 10; // 4 buffers per local talker #else xhvParams.dwMaxCompressedBuffers = 4; // 4 buffers per local talker #endif xhvParams.dwFlags = 0; xhvParams.pEffectImageDesc = getEffectsImageDesc(); xhvParams.dwEffectsStartIndex = GraphVoice_Voice_0; // Create the engine and use this object for the callbacks if( FAILED( m_XHVVoiceManager.Initialize( &xhvParams ) ) ) return E_FAIL; m_XHVVoiceManager.SetCallbackInterface( this ); // m_XHVVoiceManager.SetMaxPlaybackStreamsCount( NUM_XHV_PLAYBACK_STREAMS ); m_bInitialized = true; // Get the mutelist add/remove task started - only do this if on Live if( !logged_on || XOnlineMutelistStartup( NULL, &m_MuteListTask ) != S_OK ) m_MuteListTask = NULL; m_MuteState = MUTE_IDLE; return S_OK; } //----------------------------------------------------------------------------- // Name: Shutdown() // Desc: Shutdown the whole voice system //----------------------------------------------------------------------------- void CVoiceManager::Shutdown() { // XBL_Cleanup calls this, just in case. But if we're cleanly // shutting down from the menu system, we've already killed voice. if( !m_bInitialized ) return; // This takes us out of running - and closes down all sockets LeaveSession(); if( FAILED( m_XHVVoiceManager.Shutdown() ) ) Com_Printf( "ERROR: Couldn't shutdown XHV\n" ); // Shutdown all mutelist functionality if( m_MuteListTask ) { // Pump task to completion while( XOnlineTaskContinue( m_MuteListTask ) != XONLINETASK_S_RUNNING_IDLE ) ; XOnlineTaskClose( m_MuteListTask ); m_MuteListTask = NULL; m_MuteState = MUTE_IDLE; } m_bInitialized = false; } //----------------------------------------------------------------------------- // Name: GetVoiceMask // Desc: Returns the index of the currently set voice mask //----------------------------------------------------------------------------- int CVoiceManager::GetVoiceMask( void ) { return m_VoiceMask; } //----------------------------------------------------------------------------- // Name: SetVoiceMask // Desc: Updates the voice mask to be used //----------------------------------------------------------------------------- void CVoiceManager::SetVoiceMask( int maskIndex ) { assert( maskIndex >= 0 && maskIndex < NUM_VOICEMASKS ); m_VoiceMask = maskIndex; m_XHVVoiceManager.SetVoiceMask( IN_GetMainController(), &g_VoiceMasks[m_VoiceMask].mask ); } //----------------------------------------------------------------------------- // Name: Tick() // Desc: Called once per frame does any voice work //----------------------------------------------------------------------------- HRESULT CVoiceManager::Tick() { // VVFIXME - Can this check against m_bRunning instead? if( !m_bInitialized ) return S_OK; // Re-build our list of voice targets: UpdateVoiceTargets(); // Pump the voice engine m_XHVVoiceManager.DoWork(); // Handle net messages ProcessDirectMessage(); ProcessReliableMessage(); // Make sure we send voice data at an appropriate rate if( m_VoiceTimer < Sys_Milliseconds() ) { SendVoiceDataToAll(); } // Pump the mute list add/remove task if( m_MuteListTask ) { HRESULT hr = XOnlineTaskContinue( m_MuteListTask ); // If we're doing something, set state. If we were doing something, transition out if( hr != XONLINETASK_S_RUNNING_IDLE ) m_MuteState = MUTE_WORKING; else if( m_MuteState == MUTE_WORKING ) m_MuteState = MUTE_REFRESH; } // Get a fresh copy of the user's mute list if we need to if( m_MuteState == MUTE_REFRESH ) { UpdateMuteList(); m_MuteState = MUTE_IDLE; } return S_OK; } //----------------------------------------------------------------------------- // Name: UpdateMuteList() // Desc: Fetches the user's mute list from the Live servers, and copies it // to the local stored version. We assume that we keep our own mute flags // current, and thus don't bother to update them here. (It's a waste). //----------------------------------------------------------------------------- void CVoiceManager::UpdateMuteList( void ) { if( !logged_on ) return; XONLINETASK_HANDLE handle; HRESULT hr = XOnlineMutelistGet( IN_GetMainController(), MAX_MUTELISTUSERS, NULL, &handle, &m_MuteList[0], &m_MuteListSize ); if( hr != S_OK ) { XOnlineTaskClose( handle ); m_MuteListSize = 0; return; } do { hr = XOnlineTaskContinue( handle ); } while( hr == XONLINETASK_S_RUNNING ); XOnlineTaskClose( handle ); } //----------------------------------------------------------------------------- // Name: JoinSession() // Desc: Opens the reliable socket to the host, this puts us in the chat system // Old code then sent a Join, and then waited for an accept. And THEN // called StartVoice() to get voice running. I do that all now - hope // no other problems surface. //----------------------------------------------------------------------------- void CVoiceManager::JoinSession( void ) { // If we were already in a session, then don't do anything. if( m_bRunning ) return; // The direct socket is a non-blocking socket on port DIRECT_PORT. BOOL bSuccess = m_DirectSock.Open( CXBSocket::Type_VDP ); if( !bSuccess ) { Com_Error( ERR_FATAL, "Couldn't open VDP socket: %i\n", WSAGetLastError() ); } CXBSockAddr directAddr( INADDR_ANY, DIRECT_PORT ); INT iResult = m_DirectSock.Bind( directAddr.GetPtr() ); assert( iResult != SOCKET_ERROR ); DWORD dwNonBlocking = 1; iResult = m_DirectSock.IoCtlSocket( FIONBIO, &dwNonBlocking ); assert( iResult != SOCKET_ERROR ); // Create a reliable socket for low-bandwidth messages that need to be // sent reliably. bSuccess = m_ReliableSock.Open( CXBSocket::Type_TCP ); assert( bSuccess ); CXBSockAddr reliableAddr( INADDR_ANY, RELIABLE_PORT ); iResult = m_ReliableSock.Bind( reliableAddr.GetPtr() ); assert( iResult != SOCKET_ERROR ); iResult = m_ReliableSock.IoCtlSocket( FIONBIO, &dwNonBlocking ); assert( iResult != SOCKET_ERROR ); // Above code was in InitSockets() // Grab some useful things: bool commPresent = CommunicatorPresent(); bool speakers = VoiceThroughSpeakers(); // Being banned forces voice to be disabled: if( logged_on && !XOnlineIsUserVoiceAllowed( XBLLoggedOnUsers[ IN_GetMainController() ].xuid.dwUserFlags ) ) m_bVoiceDisabled = true; // Disabled voice implies (and overrides) no speakers: if( m_bVoiceDisabled ) { m_XHVVoiceManager.SetVoiceThroughSpeakers( FALSE ); speakers = false; } // If not disabled, we can always listen (through speakers) if( !m_bVoiceDisabled ) xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags |= VOICE_CAN_RECV; else xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags &= ~VOICE_CAN_RECV; // To talk, we need a communicator, and we need to not be banned, // and speakers needs to be off, and voice needs to be enabled: if( commPresent && !speakers && !m_bVoiceDisabled ) xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags |= VOICE_CAN_SEND; else xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags &= ~VOICE_CAN_SEND; // Open up a reliable socket to the host - this will be used // for low-bandwidth communication. We have to wait for the connection // to complete before doing anything else? if( com_sv_running->integer ) { // Start listening for new clients m_ReliableSock.Listen(); } else { // This now triggers everyone knowing that we're ready for voice CXBSockAddr saHost( xbc.SrvAddr, RELIABLE_PORT ); m_ReliableSock.Connect( saHost.GetPtr() ); // Wait for the socket to finish connecting BOOL socketWritable; while ( !m_ReliableSock.Select( NULL, &socketWritable, NULL ) ) ; // We send a voiceinfo message to tell people our voice configuration VOICEINFO vState; if( m_bVoiceDisabled ) vState = VOICEINFO_NOVOICE; else if( commPresent ) vState = VOICEINFO_HAVEVOICE; else vState = VOICEINFO_SPEAKERS; SendVoiceInfo( vState, NULL ); } // Code that used to be in StartVoice() m_msgVoiceData.GetMsgVoiceData().wVoicePackets = 0; m_VoiceTimer = Sys_Milliseconds() + VOICE_PACKET_INTERVAL; // Put communicator into voice chat mode m_XHVVoiceManager.SetProcessingMode( IN_GetMainController(), XHV_VOICECHAT_MODE ); // If we don't have a communicator, and we're not banned, send voice to speakers //m_XHVVoiceManager.SetVoiceThroughSpeakers( !commPresent && !voiceBanned ); // Finally, register everyone that's already in the game and can send or receive voice: for( int i = 0; i < MAX_ONLINE_PLAYERS; ++i ) { if( i == xbOnlineInfo.localIndex || !xbOnlineInfo.xbPlayerList[i].isActive || !(xbOnlineInfo.xbPlayerList[i].flags & VOICE_CAN_RECV) ) { continue; } // We should never get here as a server assert( !com_sv_running->integer ); OnPlayerJoined( &xbOnlineInfo.xbPlayerList[i] ); } // Reset initial channel m_Channel = CHAN_PRIMARY; // Set our voice mask from user settings: SetVoiceMask( Settings.voiceMask ); // And try to re-apply the stored voice mode: SetVoiceOptions( Settings.voiceMode ); m_bRunning = true; } //----------------------------------------------------------------------------- // Name: LeaveSession() // Desc: Blows away any leftover sockets, etc... Called when leaving a session //----------------------------------------------------------------------------- void CVoiceManager::LeaveSession( void ) { // Ensure that we're actually in a session to leave if( !m_bRunning ) return; // Clean up XHV and voice state m_XHVVoiceManager.ClearRemoteTalkers(); m_XHVVoiceManager.SetProcessingMode( IN_GetMainController(), XHV_INACTIVE_MODE ); // Close down the sockets m_DirectSock.Close(); m_ReliableSock.Close(); for( int i = 0; i < MAX_CLIENTS; ++i ) { if( m_ClientSockets[i].inUse ) { closesocket( m_ClientSockets[i].sock ); m_ClientSockets[i].inUse = false; } } /* for( SocketList::iterator it = m_ClientSockets.begin(); it < m_ClientSockets.end(); ++it ) { closesocket( it->sock ); } m_ClientSockets.clear(); */ m_bRunning = false; } //----------------------------------------------------------------------------- // Name: SendVoiceInfo() // Desc: Issue a MSG_VOICEINFO from ourself (either a host or player) to // another player //----------------------------------------------------------------------------- void CVoiceManager::SendVoiceInfo( VOICEINFO action, XBPlayerInfo* pDestPlayer ) { InfoMessage msgVoiceInfo; MsgVoiceInfo& msg = msgVoiceInfo.GetMsgVoiceInfo(); msg.action = action; msg.srcRefIndex = xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].refIndex; // Send the message reliably - whether it's sent to all players // or just to one specific player is determined by what the // caller passed in for pDestPlayer INT nBytes; if( pDestPlayer ) { // Send the voice info message reliably to the player // (note that it may be relayed by the host) msg.dstRefIndex = pDestPlayer->refIndex; CXBSockAddr sa( pDestPlayer->inAddr, DIRECT_PORT ); nBytes = SendInfoMessage( &msgVoiceInfo, &sa ); } else { // Send the voice info message reliably to all players in the game // (note that it will definitely be relayed by the host) msg.dstRefIndex = -1; nBytes = SendInfoMessage( &msgVoiceInfo ); } // This assert was removed because Send no longer is guaranteed to always work // If the security association times out, the number of bytes returned will // NOT be equal to the size of the message. A good thing to do here would // be to drop the player //assert( nBytes == SOCKET_ERROR || nBytes == msgVoiceInfo.GetSize() ); (void)nBytes; } //----------------------------------------------------------------------------- // Name: SendVoiceDataToAll // Desc: Sends accumulated voice data out to other players in the game //----------------------------------------------------------------------------- void CVoiceManager::SendVoiceDataToAll() { // Make sure we actually have data to send... if( m_msgVoiceData.GetMsgVoiceData().wVoicePackets > 0 ) { // Send voice data via VDP directly to all other players INT nBytes = SendVoiceMessage( &m_msgVoiceData ); // This assert was removed because Send no longer is guaranteed to always work // If the security association times out, the number of bytes returned will // NOT be equal to the size of the message. A good thing to do here would // be to drop the player //assert( nBytes == SOCKET_ERROR || nBytes == m_msgVoiceData.GetSize() ); (void)nBytes; } m_msgVoiceData.GetMsgVoiceData().wVoicePackets = 0; m_VoiceTimer = Sys_Milliseconds() + VOICE_PACKET_INTERVAL; } //----------------------------------------------------------------------------- // Name: LocalChatDataReady // Desc: XHV Callback - called when a packet of voice data is ready to be // sent over the wire //----------------------------------------------------------------------------- HRESULT CVoiceManager::LocalChatDataReady( DWORD dwPort, DWORD dwSize, PVOID pData ) { // If we're not in a game right now, or voice is disabled - do nothing if( !m_bRunning || m_bVoiceDisabled ) return S_OK; MsgVoiceData& msg = m_msgVoiceData.GetMsgVoiceData(); memcpy( msg.VoicePackets[ msg.wVoicePackets ].byData, pData, dwSize ); msg.wVoicePackets++; // We've set up our voice timer such that it SHOULD cause us to send out // our buffered voice data before the buffer fills up. However, things // like framerate glitches, etc., could cause us to fill up before we // notice the timer has fired. if( msg.wVoicePackets == MAX_VOICE_PER_PACKET ) { SendVoiceDataToAll(); } return S_OK; } //----------------------------------------------------------------------------- // Name: CommunicatorStatusUpdate // Desc: XHV Callback - called when the engine detects that the status of a // communicator has changed. May not be called if a communicator // is quickly removed and re-inserted, but in that case there is // nothing the game has to do. //----------------------------------------------------------------------------- HRESULT CVoiceManager::CommunicatorStatusUpdate( DWORD dwPort, XHV_VOICE_COMMUNICATOR_STATUS status ) { // If we're not initialized, then do nothing if( !m_bInitialized ) return S_OK; if( status == XHV_VOICE_COMMUNICATOR_STATUS_INSERTED ) { // Got a new headset: // Let the UI know that we have a headset, so it can enable things! Cvar_SetValue( "ui_headset", 1 ); // Awful UI hack. If we're on the online options screen, move off any item // that just became disabled. menuDef_t *menu = Menu_GetFocused(); if( menu && !Q_stricmp(menu->window.name, "xbl_onlineoptions") ) { VM_Call( uivm, UI_KEY_EVENT, A_CURSOR_DOWN, qtrue ); // Send a "move the cursor down" // Also, if we had "Speakers" selected, switch it to "Enabled" if( Cvar_VariableIntegerValue( "ui_voiceMode" ) == 1 ) Cvar_SetValue( "ui_voiceMode", 2 ); } // Always re-route voice to headset m_XHVVoiceManager.SetVoiceThroughSpeakers( FALSE ); // Don't do anything else if banned, or voice is disabled: if( m_bVoiceDisabled || (logged_on && !XOnlineIsUserVoiceAllowed( XBLLoggedOnUsers[ IN_GetMainController() ].xuid.dwUserFlags ) ) ) return S_OK; // If we're logged onto live, update our voice flag if( logged_on ) XBL_F_SetState( XONLINE_FRIENDSTATE_FLAG_VOICE, true ); // Finally, if we're in a session, update our status and tell everyone: if( m_bRunning ) { xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags |= VOICE_CAN_RECV; xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags |= VOICE_CAN_SEND; SendVoiceInfo( VOICEINFO_HAVEVOICE, NULL ); } } else if( status == XHV_VOICE_COMMUNICATOR_STATUS_REMOVED ) { // Lost a headset: // Let the UI know that we don't have a headset, so it can disable things! Cvar_SetValue( "ui_headset", 0 ); // Awful UI hack. If we're on the online options screen, move off any item // that just became disabled. menuDef_t *menu = Menu_GetFocused(); if( menu && !Q_stricmp(menu->window.name, "xbl_onlineoptions") ) { VM_Call( uivm, UI_KEY_EVENT, A_CURSOR_UP, qtrue ); // Send a "move the cursor up" // Also, if we had "Enabled" selected, that's no longer valid. Change ui_voiceMode: if( Cvar_VariableIntegerValue( "ui_voiceMode" ) == 2 ) Cvar_SetValue( "ui_voiceMode", 1 ); } // If the user pulls the headset and it was set to "speakers" or "enabled", // then change to "speakers" m_XHVVoiceManager.SetVoiceThroughSpeakers( !m_bVoiceDisabled ); // Don't do anything else if banned, or voice is disabled: if( m_bVoiceDisabled || (logged_on && !XOnlineIsUserVoiceAllowed( XBLLoggedOnUsers[ IN_GetMainController() ].xuid.dwUserFlags ) ) ) return S_OK; // If we're logged onto live, update our voice flag if( logged_on ) XBL_F_SetState( XONLINE_FRIENDSTATE_FLAG_VOICE, false ); // Finally, if we're in a session, update our status and tell everyone: if( m_bRunning ) { xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags |= VOICE_CAN_RECV; xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags &= ~VOICE_CAN_SEND; SendVoiceInfo( VOICEINFO_SPEAKERS, NULL ); } } return S_OK; } //----------------------------------------------------------------------------- // Name: SendInfoMessage // Desc: Sends a reliable InfoMessage, either to everyone (NULL) or a specific // recipient. //----------------------------------------------------------------------------- int CVoiceManager::SendInfoMessage( const InfoMessage* pMsg, const CXBSockAddr* psaDest ) { int nBytes = 0; // Reliable messages are sent via TCP connection, and so should // only be used for low-bandwidth, low-frequency messages. // Consider actively throttling the amount of data sent reliably if( com_sv_running->integer ) { // We're the host if( psaDest ) { // This is going to one player, we can send right to them MatchInAddr matchInAddr( psaDest->GetInAddr() ); // SocketList::iterator it = std::find_if( m_ClientSockets.begin(), m_ClientSockets.end(), matchInAddr ); int cIdx; for( cIdx = 0; cIdx < MAX_CLIENTS; ++cIdx ) { if( m_ClientSockets[cIdx].inUse && matchInAddr( m_ClientSockets[cIdx] ) ) break; } // assert( it != m_ClientSockets.end() ); assert( cIdx != MAX_CLIENTS ); // nBytes += send( it->sock, (char*)pMsg, pMsg->GetSize(), 0 ); nBytes += send( m_ClientSockets[cIdx].sock, (char*)pMsg, pMsg->GetSize(), 0 ); } else { // This is going to all players, so iterate over all clients and send for( int i = 0; i < MAX_CLIENTS; ++i ) { if( m_ClientSockets[i].inUse ) nBytes += send( m_ClientSockets[i].sock, (char*)pMsg, pMsg->GetSize(), 0 ); } /* for( SocketList::iterator it = m_ClientSockets.begin(); it < m_ClientSockets.end(); ++it ) { nBytes += send( it->sock, (char*)pMsg, pMsg->GetSize(), 0 ); } */ } } else { // We're a client - send to host and let him forward if needed if( m_ReliableSock.IsOpen() ) nBytes += m_ReliableSock.Send( pMsg, pMsg->GetSize() ); } return nBytes; } // Utility to verify that the given player is active, can receive voice, and // that neither of us has the other muted: bool _playerWantsVoice( const int index ) { return xbOnlineInfo.xbPlayerList[index].isActive && (xbOnlineInfo.xbPlayerList[index].flags & VOICE_CAN_RECV) && !(xbOnlineInfo.xbPlayerList[index].flags & MUTED_PLAYER) && !(xbOnlineInfo.xbPlayerList[index].flags & REMOTE_MUTED); } // Utility to add a player index/distance pair to the voice targets list: void _addVoiceTarget( const int index, unsigned long dist ) { voiceTargets[numVoiceTargets].index = index; voiceTargets[numVoiceTargets].dist = dist; ++numVoiceTargets; } //----------------------------------------------------------------------------- // Name: UpdateVoiceTargets // Desc: Re-calculates the set of people that we should send voice to //----------------------------------------------------------------------------- void CVoiceManager::UpdateVoiceTargets( void ) { // Re-set the count: numVoiceTargets = 0; // ONE: Dedicated server sends to everyone or no-one, depending on channel: if( xbOnlineInfo.localIndex == DEDICATED_SERVER_INDEX ) { if( m_Channel == CHAN_ALT ) { // Button is held, add all clients to our recipient list: for( int i = 0; i < DEDICATED_SERVER_INDEX; ++i ) { if( _playerWantsVoice( i ) ) _addVoiceTarget( i, 0 ); } } } else { // We're not the dedicated server. Now we get to all the game-type rules and such: // TWO: Everyone sends to the dedicated server, if there is one: if( _playerWantsVoice( DEDICATED_SERVER_INDEX ) ) _addVoiceTarget( DEDICATED_SERVER_INDEX, 0 ); // Now check all the other clients: for( int i = 0; i < DEDICATED_SERVER_INDEX; ++i ) { // Skip any non-active players, not listening, muted either way, and ourselves if( !_playerWantsVoice( i ) || (i == xbOnlineInfo.localIndex) ) continue; // THREE: Spectators are ALWAYS segregated from all other players: if( cgs.clientinfo[xbOnlineInfo.localIndex].team == TEAM_SPECTATOR || cgs.clientinfo[i].team == TEAM_SPECTATOR ) { if( cgs.clientinfo[i].team == cgs.clientinfo[xbOnlineInfo.localIndex].team ) _addVoiceTarget( i, 0 ); continue; } // FOUR: In team games, you can talk to teammates, or everyone: if( cgs.gametype == GT_TEAM || cgs.gametype == GT_CTF || cgs.gametype == GT_SIEGE ) { // On primary channel, only talk to teammates, on alt channel talk to all: if( (m_Channel == CHAN_ALT) || (cgs.clientinfo[i].team == cgs.clientinfo[xbOnlineInfo.localIndex].team) ) { vec3_t vecToPlayer; VectorSubtract( m_ClientPositions[i], m_ClientPositions[xbOnlineInfo.localIndex], vecToPlayer ); _addVoiceTarget( i, VectorLengthSquared( vecToPlayer ) ); } continue; } // FIVE: In duel, the two duelists can talk to each other: if( cgs.gametype == GT_DUEL ) { if( cgs.clientinfo[i].team != TEAM_SPECTATOR ) _addVoiceTarget( i, 0 ); continue; } // SIX: In power duel... if( cgs.gametype == GT_POWERDUEL ) { // Players can never talk to spectators: if( cgs.clientinfo[i].team == TEAM_SPECTATOR ) continue; // Singles can talk to enemies, Double can talk to each other, plus enemy with button: if( cgs.clientinfo[xbOnlineInfo.localIndex].duelTeam == DUELTEAM_LONE ) _addVoiceTarget( i, 0 ); else if( cgs.clientinfo[i].duelTeam == DUELTEAM_DOUBLE || m_Channel == CHAN_ALT ) _addVoiceTarget( i, 0 ); continue; } // SEVEN: FFA is all that's left, we add everyone: vec3_t vecToPlayer; VectorSubtract( m_ClientPositions[i], m_ClientPositions[xbOnlineInfo.localIndex], vecToPlayer ); _addVoiceTarget( i, VectorLengthSquared( vecToPlayer ) ); } // Remove any, if we need to. Dedicated servers and spectators skip this step: if( cgs.clientinfo[xbOnlineInfo.localIndex].team != TEAM_SPECTATOR ) { while( numVoiceTargets > maxVoiceTargets ) { int largest = 0; for( int j = 1; j < numVoiceTargets; ++j ) if( voiceTargets[j].dist > voiceTargets[largest].dist ) largest = j; --numVoiceTargets; voiceTargets[largest].index = voiceTargets[numVoiceTargets].index; voiceTargets[largest].dist = voiceTargets[numVoiceTargets].dist; } } } } //----------------------------------------------------------------------------- // Name: SendVoiceMessage // Desc: Sends a voice message over the network, to everyone (NULL) or a // specific recipient. //----------------------------------------------------------------------------- int CVoiceManager::SendVoiceMessage( const VoiceMessage* pMsg, const CXBSockAddr* psaDest ) { int nBytes = 0; // Non-reliable message - these get sent directly via VDP in all cases if( psaDest ) { // If destined for a specific player, send straight to them nBytes += m_DirectSock.SendTo( pMsg, pMsg->GetSize(), psaDest->GetPtr() ); } else { // Send to our current list of voice targets: for( int i = 0; i < numVoiceTargets; ++i ) { CXBSockAddr sa( xbOnlineInfo.xbPlayerList[voiceTargets[i].index].inAddr, DIRECT_PORT ); nBytes += m_DirectSock.SendTo( pMsg, pMsg->GetSize(), sa.GetPtr() ); } } return nBytes; } //----------------------------------------------------------------------------- // Name: ProcessDirectMessage() // Desc: Checks to see if any direct messages are waiting on the direct socket. // These can ONLY be VoiceMessages. // If a message is waiting, it is routed and processed. // If no messages are waiting, the function returns immediately. //----------------------------------------------------------------------------- void CVoiceManager::ProcessDirectMessage() { if( !m_DirectSock.IsOpen() ) return; // See if a network message is waiting for us VoiceMessage msg; SOCKADDR_IN saFromIn; INT iResult; // Process until no more messages are available do { iResult = m_DirectSock.RecvFrom( &msg, msg.GetSize(), &saFromIn ); CXBSockAddr saFrom( saFromIn ); // If message waiting, process it if( iResult != SOCKET_ERROR && iResult > 0 ) { assert( iResult == msg.GetSize() ); ProcessVoiceData( msg.GetMsgVoiceData(), saFrom ); } else { assert( WSAGetLastError() == WSAEWOULDBLOCK ); } } while( iResult != SOCKET_ERROR && iResult > 0 ); return; } //----------------------------------------------------------------------------- // Name: Read // Desc: Attempts to read a message from the specified socket. Our reliable // sockets are stream-oriented, so the message may come in small pieces. // Fortunately, all reliable messages are the same size, so the logic is // pretty simple. // Returns TRUE is completely parsed. //----------------------------------------------------------------------------- HRESULT PendingMessage::Read( SOCKET sock ) { // If we have a partial message, just ask for enough data to complete it if( m_nBytesReceived < m_msg.GetSize() ) { CHAR* pbReceive = ( (CHAR *)&m_msg ) + m_nBytesReceived; INT nBytesReq = m_msg.GetSize() - m_nBytesReceived; INT nBytes = recv( sock, pbReceive, nBytesReq, 0 ); // Check result if( nBytes == SOCKET_ERROR ) { if( WSAGetLastError() != WSAEWOULDBLOCK ) return E_FAIL; } else { m_nBytesReceived += nBytes; } } // Determine if we now have a complete message if( m_nBytesReceived == m_msg.GetSize() ) { return S_OK; } else { return S_FALSE; } } //----------------------------------------------------------------------------- // Name: ProcessReliableMessage() // Desc: First checks to see if any new connections have been attempted, and if // we have room, accepts them. Then, scans all reliable client // connections to see if any have messages pending. // If a message is waiting, it is routed and processed. // If no messages are waiting, the function returns immediately. //----------------------------------------------------------------------------- void CVoiceManager::ProcessReliableMessage() { if( !m_ReliableSock.IsOpen() ) return; if( com_sv_running->integer ) { // Process any pending socket connections for( ; ; ) { ClientSocket cs; cs.sock = m_ReliableSock.Accept( &cs.sa ); if( cs.sock == INVALID_SOCKET ) break; int cIdx; for( cIdx = 0; cIdx < MAX_CLIENTS; ++cIdx ) { if( !m_ClientSockets[cIdx].inUse ) break; } assert( cIdx != MAX_CLIENTS ); m_ClientSockets[cIdx] = cs; m_ClientSockets[cIdx].inUse = true; // m_ClientSockets.push_back( cs ); } // Poll each of our clients for messages and timeout // for( SocketList::iterator it = m_ClientSockets.begin(); // it < m_ClientSockets.end(); // ++it ) for( int i = 0; i < MAX_CLIENTS; ++i ) { if( !m_ClientSockets[i].inUse ) continue; ClientSocket *it = &m_ClientSockets[i]; // Try to parse out a message from the socket. If message was // completed, process the message HRESULT hr = it->msgPending.Read( it->sock ); if( FAILED( hr ) ) { // We lost a connection to a client. Com_Printf( "WARNING: Reliable voice cnxn was lost\n" ); // VVFIXME - This code assumes that the xbPlayerInfo for the drop is // still valid. It might not be, in some really outrageous situation. // We should figure out a solution to that. // Remove them from the Voice System, but let the game logic handle // everything else (xbOnlineInfo). int idx; for( idx = 0; idx < MAX_ONLINE_PLAYERS; ++idx ) { if( xbOnlineInfo.xbPlayerList[idx].isActive && xbOnlineInfo.xbPlayerList[idx].inAddr.s_addr == it->sa.sin_addr.s_addr ) break; } if( idx == MAX_ONLINE_PLAYERS ) { // This shouldn't happen (but it can - see above) Com_Error( ERR_FATAL, "ERROR: Couldn't find user after reliable cnxn lost\n" ); } OnPlayerDisconnect( &xbOnlineInfo.xbPlayerList[idx] ); // OK. We found them in the playerlist. Take them out of the voice system // This function removes the socket from the vector. I don't care if SimpleVoice // isn't iterator safe - I will be. =) VVFIXME - I still don't like this. /* int oldIndex = it - m_ClientSockets.begin(); OnPlayerDisconnect( &xbOnlineInfo.xbPlayerList[idx] ); it = m_ClientSockets.begin() + oldIndex; */ } else if( S_OK == hr ) { ProcessVoiceInfo( it->msgPending.m_msg.GetMsgVoiceInfo(), CXBSockAddr( it->sa ) ); it->msgPending.Reset(); } } } else { HRESULT hr = m_msgPending.Read( m_ReliableSock.GetSocket() ); if( FAILED( hr ) ) { // This leads to calling g_Voice.Shutdown(), let that handle cleanup Com_Error( ERR_DROP, "@MENUS_LOST_CONNECTION" ); } else if( S_OK == hr ) { ProcessVoiceInfo( m_msgPending.m_msg.GetMsgVoiceInfo(), CXBSockAddr( xbc.SrvAddr, RELIABLE_PORT ) ); m_msgPending.Reset(); } } return; } //----------------------------------------------------------------------------- // Name: ProcessVoiceInfo() // Desc: Process the voiceport message //----------------------------------------------------------------------------- void CVoiceManager::ProcessVoiceInfo( const MsgVoiceInfo& msg, const CXBSockAddr& saFrom ) { // We can't just look at the INADDR of the sender, since this message // may have been relayed by the host int idx; for ( idx = 0; idx < MAX_ONLINE_PLAYERS; ++idx ) { if( xbOnlineInfo.xbPlayerList[idx].isActive && xbOnlineInfo.xbPlayerList[idx].refIndex == msg.srcRefIndex ) break; } // If we get a message from an invalid player, ignore it if( idx == MAX_ONLINE_PLAYERS ) { Com_Printf( "ERROR: VoiceInfo from invalid player!\n" ); return; } XBPlayerInfo *srcPlyr = &xbOnlineInfo.xbPlayerList[idx]; // This message may or may not be intended for us. If there's no // destination player specified, it's meant for everyone. Otherwise, // we should only process it if it's got our name on it if( msg.dstRefIndex == -1 || msg.dstRefIndex == xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].refIndex ) { switch( msg.action ) { case VOICEINFO_NOVOICE: // This player wants no voice, or has been banned srcPlyr->flags &= ~VOICE_CAN_SEND; srcPlyr->flags &= ~VOICE_CAN_RECV; break; case VOICEINFO_SPEAKERS: // This player will only be receiving voice, not sending it srcPlyr->flags &= ~VOICE_CAN_SEND; srcPlyr->flags |= VOICE_CAN_RECV; // We still register them with XHV, to check against our mute list OnPlayerJoined( srcPlyr ); break; case VOICEINFO_HAVEVOICE: // This player will be sending and receiving voice srcPlyr->flags |= VOICE_CAN_SEND; srcPlyr->flags |= VOICE_CAN_RECV; // Register them with XHV, and check mute lists OnPlayerJoined( srcPlyr ); break; case VOICEINFO_ADDREMOTEMUTE: // We've been muted by someone srcPlyr->flags |= REMOTE_MUTED; m_XHVVoiceManager.SetRemoteMute( srcPlyr->xuid, IN_GetMainController(), TRUE ); break; case VOICEINFO_REMOVEREMOTEMUTE: // We've been un-muted by someone srcPlyr->flags &= ~(REMOTE_MUTED); m_XHVVoiceManager.SetRemoteMute( srcPlyr->xuid, IN_GetMainController(), FALSE ); break; default: assert( FALSE ); break; } } // If the host wasn't the recipient of this message, or the message // is intended for all players, it's the host's responsibility to // relay it to all clients if( com_sv_running->integer ) { InfoMessage msgVoiceInfo; msgVoiceInfo.GetMsgVoiceInfo() = msg; if( msg.dstRefIndex == -1 ) { // Intended for everyone - send to all BUT the source // for( SocketList::iterator it = m_ClientSockets.begin(); // it < m_ClientSockets.end(); // ++it ) for( int i = 0; i < MAX_CLIENTS; ++i ) { if( !m_ClientSockets[i].inUse ) continue; ClientSocket *it = &m_ClientSockets[i]; if( it->sa.sin_addr.s_addr != saFrom.GetInAddr().s_addr ) { CXBSockAddr saDest( it->sa ); SendInfoMessage( &msgVoiceInfo, &saDest ); } } } else if( msg.dstRefIndex != xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].refIndex ) { // Intended for a specific player (not us) - Find that player and send to them for( int i = 0; i < MAX_ONLINE_PLAYERS; ++i ) { if( xbOnlineInfo.xbPlayerList[i].isActive && xbOnlineInfo.xbPlayerList[i].refIndex == msg.dstRefIndex ) { CXBSockAddr saDest( xbOnlineInfo.xbPlayerList[i].inAddr, RELIABLE_PORT ); SendInfoMessage( &msgVoiceInfo, &saDest ); } } } } } //----------------------------------------------------------------------------- // Name: ProcessVoiceData // Desc: Handles receipt of a voice data packet //----------------------------------------------------------------------------- void CVoiceManager::ProcessVoiceData( const MsgVoiceData& msg, const CXBSockAddr& saFrom ) { for( WORD i = 0; i < msg.wVoicePackets; i++ ) { const VoicePacket* pPacket = &msg.VoicePackets[i]; // We need to find the sender in the player list, so we can get their XUID int j; for( j = 0; j < MAX_ONLINE_PLAYERS; ++j ) { if( xbOnlineInfo.xbPlayerList[j].isActive && xbOnlineInfo.xbPlayerList[j].inAddr.s_addr == saFrom.GetInAddr().s_addr ) break; } if( j == MAX_ONLINE_PLAYERS) { Com_Printf( "ERROR: Voice data from invalid IN_ADDR!\n" ); return; } if( xbOnlineInfo.xbPlayerList[j].flags & MUTED_PLAYER || xbOnlineInfo.xbPlayerList[j].flags & REMOTE_MUTED ) { // Don't let late voice play after a mute was set return; } m_XHVVoiceManager.SubmitIncomingVoicePacket( xbOnlineInfo.xbPlayerList[j].xuid, (VOID*)pPacket->byData, COMPRESSED_VOICE_SIZE ); } } //----------------------------------------------------------------------------- // Name: OnPlayerJoined // Desc: Called whenever someone is going to start sending or receiving chat. // Originally intended for first-join only, but now it re-checks mute // lists and such as well. //----------------------------------------------------------------------------- HRESULT CVoiceManager::OnPlayerJoined( XBPlayerInfo *plyrInfo ) { // Register the new player with XHV m_XHVVoiceManager.RegisterRemoteTalker( plyrInfo->xuid ); // VVFIXME - fancier priority scheme needed! m_XHVVoiceManager.SetRemoteTalkerPriority( plyrInfo->xuid, IN_GetMainController(), XHV_PLAYBACK_PRIORITY_MAX ); // Scan for the player in our mute list for( int i = 0; i < m_MuteListSize; ++i ) { if( XOnlineAreUsersIdentical( &m_MuteList[i].xuid, &plyrInfo->xuid ) ) { // OK. We don't like this person. Mark them muted, and tell them. plyrInfo->flags |= MUTED_PLAYER; m_XHVVoiceManager.SetMute( plyrInfo->xuid, IN_GetMainController(), TRUE ); SendVoiceInfo( VOICEINFO_ADDREMOTEMUTE, plyrInfo ); break; } } return S_OK; } //----------------------------------------------------------------------------- // Name: OnPlayerDisconnect // Desc: Called whenever we've detected a player disconnect. Should be called // before the XBPlayerInfo is blown away. This is NOT symmetric with // OnPlayerJoined, which can be called many times during a game. //----------------------------------------------------------------------------- HRESULT CVoiceManager::OnPlayerDisconnect( XBPlayerInfo *pPlayer ) { // The host needs to close down the reliable channel if( com_sv_running->integer ) { // Find the matching entry in list of reliable sockets MatchInAddr matchInAddr( pPlayer->inAddr ); // SocketList::iterator it = std::find_if( m_ClientSockets.begin(), m_ClientSockets.end(), matchInAddr ); int cIdx; for( cIdx = 0; cIdx < MAX_CLIENTS; ++cIdx ) { if( m_ClientSockets[cIdx].inUse && matchInAddr( m_ClientSockets[cIdx] ) ) break; } // Not finding the socket was an error before (in SimpleVoice). But if the client // just disconnects, we'll call this as soon as we notice the socket is hosed, and // again when we get the removepeer message after the net core times out the client. // if( it != m_ClientSockets.end() ) if( cIdx != MAX_CLIENTS ) { ClientSocket *it = &m_ClientSockets[cIdx]; // Close the socket closesocket( it->sock ); // m_ClientSockets.erase( it ); it->inUse = false; } } // Notify XHV that the player is gone m_XHVVoiceManager.UnregisterRemoteTalker( pPlayer->xuid ); // Don't try and send them any more voice packets pPlayer->flags &= ~VOICE_CAN_RECV; pPlayer->flags &= ~VOICE_CAN_SEND; return S_OK; } //----------------------------------------------------------------------------- // Name: CommunicatorPresent // Desc: Tells whether or not we have a communicator plugged in //----------------------------------------------------------------------------- bool CVoiceManager::CommunicatorPresent( void ) { XHV_LOCAL_TALKER_STATUS status; m_XHVVoiceManager.GetLocalTalkerStatus( IN_GetMainController(), &status ); return status.communicatorStatus == XHV_VOICE_COMMUNICATOR_STATUS_INSERTED; } //----------------------------------------------------------------------------- // Name: IsVoiceAllowed // Desc: Do we have a communicator, and if so, should we use it? //----------------------------------------------------------------------------- bool CVoiceManager::IsVoiceAllowed( void ) { // Without a headset, or with voice disabled, we can't talk. if( !CommunicatorPresent() || m_bVoiceDisabled ) return false; // OK. We have a headset, and we're on syslink, so it's good. if( !logged_on ) return true; return XOnlineIsUserVoiceAllowed( XBLLoggedOnUsers[ IN_GetMainController() ].xuid.dwUserFlags ); } //----------------------------------------------------------------------------- // Name: SetMute // Desc: Sets the mute state for the given user to the specified state. // Updates XHV, and also our user's global mute list (if logged on). //----------------------------------------------------------------------------- void CVoiceManager::SetMute( XUID xuid, BOOL bMuted ) { // First, set the mute state in XHV m_XHVVoiceManager.SetMute( xuid, IN_GetMainController(), bMuted ); // If we're logged on, we need to update our global list if( logged_on ) { // Make the change to our mutelist if( bMuted ) XOnlineMutelistAdd( IN_GetMainController(), xuid ); else XOnlineMutelistRemove( IN_GetMainController(), xuid ); // Signal that we've made a change. This forces Tick() to refresh // our list, just in case. m_MuteState = MUTE_WORKING; } // If the user is in our game currently, send them a message: int idx; for( idx = 0; idx < MAX_ONLINE_PLAYERS; ++idx ) { if( xbOnlineInfo.xbPlayerList[idx].isActive && XOnlineAreUsersIdentical( &xuid, &xbOnlineInfo.xbPlayerList[idx].xuid ) ) break; } // If they're not in the game, nothing else to do if( idx == MAX_ONLINE_PLAYERS ) return; // Update our flags, and let the other person know: XBPlayerInfo *plyrInfo = &xbOnlineInfo.xbPlayerList[idx]; if( bMuted ) { plyrInfo->flags |= MUTED_PLAYER; SendVoiceInfo( VOICEINFO_ADDREMOTEMUTE, plyrInfo ); } else { plyrInfo->flags &= ~(MUTED_PLAYER); SendVoiceInfo( VOICEINFO_REMOVEREMOTEMUTE, plyrInfo ); } return; } //----------------------------------------------------------------------------- // Name: IsMuted // Desc: Returns whether or not the specificed player is muted by the current // player. This works for anyone, not just people in the current game, so // the friends UI can use it. //----------------------------------------------------------------------------- bool CVoiceManager::IsMuted( XUID xuid ) { for( int i = 0; i < m_MuteListSize; ++i ) { if( XOnlineAreUsersIdentical( &xuid, &m_MuteList[i].xuid ) ) return true; } return false; } //----------------------------------------------------------------------------- // Name: SetChannel // Desc: Controls how our voice is sent out. There are only two channels: // CHAN_PRIMARY and CHAN_ALT, and their meaning depends on the game // mode, and lots of other things. //----------------------------------------------------------------------------- void CVoiceManager::SetChannel( VoiceChannel chan ) { m_Channel = chan; } //----------------------------------------------------------------------------- // Name: GetVoiceDisabled // Desc: Returns the state of the master cutoff //----------------------------------------------------------------------------- bool CVoiceManager::GetVoiceDisabled( void ) { return m_bVoiceDisabled; } //----------------------------------------------------------------------------- // Name: SetVoiceOptions // Desc: Used by the UI to find out the current voice mode: // 0 (disabled), 1 (speakers), 2 (enabled) //----------------------------------------------------------------------------- int CVoiceManager::GetVoiceMode( void ) { if( m_bVoiceDisabled ) return 0; if( m_XHVVoiceManager.GetVoiceThroughSpeakers() || !CommunicatorPresent() ) return 1; return 2; } //----------------------------------------------------------------------------- // Name: SetVoiceOptions // Desc: Used by the UI to adjust voice through speakers and voice disable // voiceMode: 0 (disabled), 1 (speakers), 2 (enabled) //----------------------------------------------------------------------------- void CVoiceManager::SetVoiceOptions( int voiceMode ) { // Do nothing if we are banned - this might not be right if( logged_on && !XOnlineIsUserVoiceAllowed( XBLLoggedOnUsers[ IN_GetMainController() ].xuid.dwUserFlags ) ) return; // This should never happen, but... bool commPresent = CommunicatorPresent(); if( !commPresent && (voiceMode == 2) ) voiceMode = 1; // Store new settings: m_bVoiceDisabled = (voiceMode == 0); m_XHVVoiceManager.SetVoiceThroughSpeakers( (voiceMode == 1) ); // Update our friend notification: we have voice if not disabled, and we have a comm: if( logged_on ) XBL_F_SetState( XONLINE_FRIENDSTATE_FLAG_VOICE, (commPresent && !m_bVoiceDisabled) ); // Update our own player information, and inform other players if we're in a session if( m_bRunning ) { // We can receive voice as long as it's not disabled: if( !m_bVoiceDisabled ) xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags |= VOICE_CAN_RECV; else xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags &= ~VOICE_CAN_RECV; // We can send voice only if it's really enabled (implies that we have a comm): if( voiceMode == 2 ) xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags |= VOICE_CAN_SEND; else xbOnlineInfo.xbPlayerList[xbOnlineInfo.localIndex].flags &= ~VOICE_CAN_SEND; // Send our new state to everyone, if we have a communicator, we always // send HAVEVOICE, for TCR reasons: if( commPresent && !m_bVoiceDisabled ) SendVoiceInfo( VOICEINFO_HAVEVOICE, NULL ); else SendVoiceInfo( (VOICEINFO)voiceMode, NULL ); } } #ifndef FINAL_BUILD void CVoiceManager::DrawVoiceStats( void ) { const float scale = 0.8f; int lineHeight = RE_Font_HeightPixels( 1, scale ); // Draw the list of other players, color coded int yPos = 0; for( int i = 0; i < MAX_ONLINE_PLAYERS; ++i ) { if( !xbOnlineInfo.xbPlayerList[i].isActive ) continue; bool bTarget = false; for( int j = 0; j < numVoiceTargets; ++j ) if( voiceTargets[j].index == i ) { bTarget = true; break; } RE_Font_DrawString( 320, 100 + (yPos * lineHeight), xbOnlineInfo.xbPlayerList[i].name, bTarget ? colorGreen : colorWhite, 1, -1, scale ); yPos++; } } #endif