diff options
Diffstat (limited to 'engine/hltvserver.cpp')
| -rw-r--r-- | engine/hltvserver.cpp | 2426 |
1 files changed, 2426 insertions, 0 deletions
diff --git a/engine/hltvserver.cpp b/engine/hltvserver.cpp new file mode 100644 index 0000000..0b4d869 --- /dev/null +++ b/engine/hltvserver.cpp @@ -0,0 +1,2426 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// hltvserver.cpp: implementation of the CHLTVServer class. +// +////////////////////////////////////////////////////////////////////// + +#include <server_class.h> +#include <inetmessage.h> +#include <tier0/vprof.h> +#include <tier0/vcrmode.h> +#include <KeyValues.h> +#include <edict.h> +#include <eiface.h> +#include <PlayerState.h> +#include <ihltvdirector.h> +#include <time.h> + +#include "hltvserver.h" +#include "sv_client.h" +#include "hltvclient.h" +#include "server.h" +#include "sv_main.h" +#include "framesnapshot.h" +#include "networkstringtable.h" +#include "cmodel_engine.h" +#include "dt_recv_eng.h" +#include "cdll_engine_int.h" +#include "GameEventManager.h" +#include "host.h" +#include "proto_version.h" +#include "proto_oob.h" +#include "dt_common_eng.h" +#include "baseautocompletefilelist.h" +#include "sv_steamauth.h" +#include "tier0/icommandline.h" +#include "sys_dll.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define S2A_EXTRA_DATA_HAS_GAME_PORT 0x80 // Next 2 bytes include the game port. +#define S2A_EXTRA_DATA_HAS_SPECTATOR_DATA 0x40 // Next 2 bytes include the spectator port, then the spectator server name. +#define S2A_EXTRA_DATA_HAS_GAMETAG_DATA 0x20 // Next bytes are the game tag string +#define S2A_EXTRA_DATA_HAS_STEAMID 0x10 // Next 8 bytes are the steamID +#define S2A_EXTRA_DATA_GAMEID 0x01 // Next 8 bytes are the gameID of the server + +#define A2S_KEY_STRING_STEAM "Source Engine Query" // required postfix to a A2S_INFO query + +extern CNetworkStringTableContainer *networkStringTableContainerClient; +extern ConVar sv_tags; + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +CHLTVServer *hltv = NULL; + +static void tv_title_changed_f( IConVar *var, const char *pOldString, float flOldValue ) +{ + if ( hltv && hltv->IsActive() ) + { + hltv->BroadcastLocalTitle(); + } +} + +static void tv_name_changed_f( IConVar *var, const char *pOldValue, float flOldValue ) +{ + Steam3Server().NotifyOfServerNameChange(); +} + +static ConVar tv_maxclients( "tv_maxclients", "128", 0, "Maximum client number on SourceTV server.", + true, 0, true, 255 ); + +ConVar tv_autorecord( "tv_autorecord", "0", 0, "Automatically records all games as SourceTV demos." ); +ConVar tv_name( "tv_name", "SourceTV", 0, "SourceTV host name", tv_name_changed_f ); +static ConVar tv_password( "tv_password", "", FCVAR_NOTIFY | FCVAR_PROTECTED | FCVAR_DONTRECORD, "SourceTV password for all clients" ); + +static ConVar tv_overridemaster( "tv_overridemaster", "0", 0, "Overrides the SourceTV master root address." ); +static ConVar tv_dispatchmode( "tv_dispatchmode", "1", 0, "Dispatch clients to relay proxies: 0=never, 1=if appropriate, 2=always" ); +ConVar tv_transmitall( "tv_transmitall", "0", FCVAR_REPLICATED, "Transmit all entities (not only director view)" ); +ConVar tv_debug( "tv_debug", "0", 0, "SourceTV debug info." ); +ConVar tv_title( "tv_title", "SourceTV", 0, "Set title for SourceTV spectator UI", tv_title_changed_f ); +static ConVar tv_deltacache( "tv_deltacache", "2", 0, "Enable delta entity bit stream cache" ); +static ConVar tv_relayvoice( "tv_relayvoice", "1", 0, "Relay voice data: 0=off, 1=on" ); + +CDeltaEntityCache::CDeltaEntityCache() +{ + Q_memset( m_Cache, 0, sizeof(m_Cache) ); + m_nTick = 0; + m_nMaxEntities = 0; + m_nCacheSize = 0; +} + +CDeltaEntityCache::~CDeltaEntityCache() +{ + Flush(); +} + +void CDeltaEntityCache::Flush() +{ + if ( m_nMaxEntities != 0 ) + { + // at least one entity was set + for ( int i=0; i<m_nMaxEntities; i++ ) + { + if ( m_Cache[i] != NULL ) + { + free( m_Cache[i] ); + m_Cache[i] = NULL; + } + } + + m_nMaxEntities = 0; + } + + m_nCacheSize = 0; +} + +void CDeltaEntityCache::SetTick( int nTick, int nMaxEntities ) +{ + if ( nTick == m_nTick ) + return; + + Flush(); + + m_nCacheSize = tv_deltacache.GetInt() * 1024; + + if ( m_nCacheSize <= 0 ) + return; + + m_nMaxEntities = min(nMaxEntities,MAX_EDICTS); + m_nTick = nTick; +} + +unsigned char* CDeltaEntityCache::FindDeltaBits( int nEntityIndex, int nDeltaTick, int &nBits ) +{ + nBits = -1; + + if ( nEntityIndex < 0 || nEntityIndex >= m_nMaxEntities ) + return NULL; + + DeltaEntityEntry_s *pEntry = m_Cache[nEntityIndex]; + + while ( pEntry ) + { + if ( pEntry->nDeltaTick == nDeltaTick ) + { + nBits = pEntry->nBits; + return (unsigned char*)(pEntry) + sizeof(DeltaEntityEntry_s); + } + else + { + // keep searching entry list + pEntry = pEntry->pNext; + } + } + + return NULL; +} + +void CDeltaEntityCache::AddDeltaBits( int nEntityIndex, int nDeltaTick, int nBits, bf_write *pBuffer ) +{ + if ( nEntityIndex < 0 || nEntityIndex >= m_nMaxEntities || m_nCacheSize <= 0 ) + return; + + int nBufferSize = PAD_NUMBER( Bits2Bytes(nBits), 4); + + DeltaEntityEntry_s *pEntry = m_Cache[nEntityIndex]; + + if ( pEntry == NULL ) + { + if ( (int)(nBufferSize+sizeof(DeltaEntityEntry_s)) > m_nCacheSize ) + return; // way too big, don't even create an entry + + pEntry = m_Cache[nEntityIndex] = (DeltaEntityEntry_s *) malloc( m_nCacheSize ); + } + else + { + char *pEnd = (char*)(pEntry) + m_nCacheSize; // end marker + + while( pEntry->pNext ) + { + pEntry = pEntry->pNext; + } + + int entrySize = sizeof(DeltaEntityEntry_s) + PAD_NUMBER( Bits2Bytes(pEntry->nBits), 4); + + DeltaEntityEntry_s *pNew = (DeltaEntityEntry_s*)((char*)(pEntry) + entrySize); + + if ( ((char*)(pNew) + sizeof(DeltaEntityEntry_s) + nBufferSize) > pEnd ) + return; // data wouldn't fit into cache anymore, don't add new entries + + pEntry = pNew; + pEntry->pNext = pEntry; + } + + pEntry->pNext = NULL; // link to next + pEntry->nDeltaTick = nDeltaTick; + pEntry->nBits = nBits; + + if ( nBits > 0 ) + { + bf_read inBuffer; + inBuffer.StartReading( pBuffer->GetData(), pBuffer->m_nDataBytes, pBuffer->GetNumBitsWritten() ); + bf_write outBuffer( (char*)(pEntry) + sizeof(DeltaEntityEntry_s), nBufferSize ); + outBuffer.WriteBitsFromBuffer( &inBuffer, nBits ); + } +} + + +static RecvTable* FindRecvTable( const char *pName, RecvTable **pRecvTables, int nRecvTables ) +{ + for ( int i=0; i< nRecvTables; i++ ) + { + if ( !Q_strcmp( pName, pRecvTables[i]->GetName() ) ) + return pRecvTables[i]; + } + + return NULL; +} + +static RecvTable* AddRecvTableR( SendTable *sendt, RecvTable **pRecvTables, int &nRecvTables ) +{ + RecvTable *recvt = FindRecvTable( sendt->m_pNetTableName, pRecvTables, nRecvTables ); + + if ( recvt ) + return recvt; // already in list + + if ( sendt->m_nProps > 0 ) + { + RecvProp *receiveProps = new RecvProp[sendt->m_nProps]; + + for ( int i=0; i < sendt->m_nProps; i++ ) + { + // copy property data + + SendProp * sp = sendt->GetProp( i ); + RecvProp * rp = &receiveProps[i]; + + rp->m_pVarName = sp->m_pVarName; + rp->m_RecvType = sp->m_Type; + + if ( sp->IsExcludeProp() ) + { + // if prop is excluded, give different name + rp->m_pVarName = "IsExcludedProp"; + } + + if ( sp->IsInsideArray() ) + { + rp->SetInsideArray(); + rp->m_pVarName = "InsideArrayProp"; // give different name + } + + if ( sp->GetType() == DPT_Array ) + { + Assert ( sp->GetArrayProp() == sendt->GetProp( i-1 ) ); + Assert( receiveProps[i-1].IsInsideArray() ); + + rp->SetArrayProp( &receiveProps[i-1] ); + rp->InitArray( sp->m_nElements, sp->m_ElementStride ); + } + + if ( sp->GetType() == DPT_DataTable ) + { + // recursive create + Assert ( sp->GetDataTable() ); + RecvTable *subTable = AddRecvTableR( sp->GetDataTable(), pRecvTables, nRecvTables ); + rp->SetDataTable( subTable ); + } + } + + recvt = new RecvTable( receiveProps, sendt->m_nProps, sendt->m_pNetTableName ); + } + else + { + // table with no properties + recvt = new RecvTable( NULL, 0, sendt->m_pNetTableName ); + } + + pRecvTables[nRecvTables] = recvt; + nRecvTables++; + + return recvt; +} + +void CHLTVServer::FreeClientRecvTables() +{ + for ( int i=0; i< m_nRecvTables; i++ ) + { + RecvTable *rt = m_pRecvTables[i]; + + // delete recv table props + if ( rt->m_pProps ) + { + Assert( rt->m_nProps > 0 ); + delete [] rt->m_pProps; + } + + // delete the table itself + delete rt; + + } + + Q_memset( m_pRecvTables, 0, sizeof( m_pRecvTables ) ); + m_nRecvTables = 0; +} + +// creates client receive tables from server send tables +void CHLTVServer::InitClientRecvTables() +{ + ServerClass* pCur = NULL; + + if ( ClientDLL_GetAllClasses() != NULL ) + return; //already initialized + + // first create all SendTables + for ( pCur = serverGameDLL->GetAllServerClasses(); pCur; pCur=pCur->m_pNext ) + { + // create receive table from send table. + AddRecvTableR( pCur->m_pTable, m_pRecvTables, m_nRecvTables ); + + ErrorIfNot( + m_nRecvTables < ARRAYSIZE( m_pRecvTables ), + ("AddRecvTableR: overflowed MAX_DATATABLES") + ); + } + + // now register client classes + for ( pCur = serverGameDLL->GetAllServerClasses(); pCur; pCur=pCur->m_pNext ) + { + ErrorIfNot( + m_nRecvTables < ARRAYSIZE( m_pRecvTables ), + ("ClientDLL_InitRecvTableMgr: overflowed MAX_DATATABLES") + ); + + // find top receive table for class + RecvTable * recvt = FindRecvTable( pCur->m_pTable->GetName(), m_pRecvTables, m_nRecvTables ); + + Assert ( recvt ); + + // register class, constructor addes clientClass to g_pClientClassHead list + ClientClass * clientclass = new ClientClass( pCur->m_pNetworkName, NULL, NULL, recvt ); + + if ( !clientclass ) + { + Msg("HLTV_InitRecvTableMgr: failed to allocate client class %s.\n", pCur->m_pNetworkName ); + return; + } + } + + RecvTable_Init( m_pRecvTables, m_nRecvTables ); +} + + + +CHLTVFrame::CHLTVFrame() +{ + +} + +CHLTVFrame::~CHLTVFrame() +{ + FreeBuffers(); +} + +void CHLTVFrame::Reset( void ) +{ + for ( int i=0; i<HLTV_BUFFER_MAX; i++ ) + { + m_Messages[i].Reset(); + } +} + +bool CHLTVFrame::HasData( void ) +{ + for ( int i=0; i<HLTV_BUFFER_MAX; i++ ) + { + if ( m_Messages[i].GetNumBitsWritten() > 0 ) + return true; + } + + return false; +} + +void CHLTVFrame::CopyHLTVData( CHLTVFrame &frame ) +{ + // copy reliable messages + int bits = frame.m_Messages[HLTV_BUFFER_RELIABLE].GetNumBitsWritten(); + + if ( bits > 0 ) + { + int bytes = PAD_NUMBER( Bits2Bytes(bits), 4 ); + m_Messages[HLTV_BUFFER_RELIABLE].StartWriting( new char[ bytes ], bytes, bits ); + Q_memcpy( m_Messages[HLTV_BUFFER_RELIABLE].GetBasePointer(), frame.m_Messages[HLTV_BUFFER_RELIABLE].GetBasePointer(), bytes ); + } + + // copy unreliable messages + bits = frame.m_Messages[HLTV_BUFFER_UNRELIABLE].GetNumBitsWritten(); + bits += frame.m_Messages[HLTV_BUFFER_TEMPENTS].GetNumBitsWritten(); + bits += frame.m_Messages[HLTV_BUFFER_SOUNDS].GetNumBitsWritten(); + + if ( tv_relayvoice.GetBool() ) + bits += frame.m_Messages[HLTV_BUFFER_VOICE].GetNumBitsWritten(); + + if ( bits > 0 ) + { + // collapse all unreliable buffers in one + int bytes = PAD_NUMBER( Bits2Bytes(bits), 4 ); + m_Messages[HLTV_BUFFER_UNRELIABLE].StartWriting( new char[ bytes ], bytes ); + m_Messages[HLTV_BUFFER_UNRELIABLE].WriteBits( frame.m_Messages[HLTV_BUFFER_UNRELIABLE].GetData(), frame.m_Messages[HLTV_BUFFER_UNRELIABLE].GetNumBitsWritten() ); + m_Messages[HLTV_BUFFER_UNRELIABLE].WriteBits( frame.m_Messages[HLTV_BUFFER_TEMPENTS].GetData(), frame.m_Messages[HLTV_BUFFER_TEMPENTS].GetNumBitsWritten() ); + m_Messages[HLTV_BUFFER_UNRELIABLE].WriteBits( frame.m_Messages[HLTV_BUFFER_SOUNDS].GetData(), frame.m_Messages[HLTV_BUFFER_SOUNDS].GetNumBitsWritten() ); + + if ( tv_relayvoice.GetBool() ) + m_Messages[HLTV_BUFFER_UNRELIABLE].WriteBits( frame.m_Messages[HLTV_BUFFER_VOICE].GetData(), frame.m_Messages[HLTV_BUFFER_VOICE].GetNumBitsWritten() ); + } +} + +void CHLTVFrame::AllocBuffers( void ) +{ + // allocate buffers for input frame + for ( int i=0; i < HLTV_BUFFER_MAX; i++ ) + { + Assert( m_Messages[i].GetBasePointer() == NULL ); + m_Messages[i].StartWriting( new char[NET_MAX_PAYLOAD], NET_MAX_PAYLOAD); + } +} + +void CHLTVFrame::FreeBuffers( void ) +{ + for ( int i=0; i<HLTV_BUFFER_MAX; i++ ) + { + bf_write &msg = m_Messages[i]; + + if ( msg.GetBasePointer() ) + { + delete[] msg.GetBasePointer(); + msg.StartWriting( NULL, 0 ); + } + } +} + +CHLTVServer::CHLTVServer() +{ + m_flTickInterval = 0.03; + m_MasterClient = NULL; + m_Server = NULL; + m_Director = NULL; + m_nFirstTick = -1; + m_nLastTick = 0; + m_CurrentFrame = NULL; + m_nViewEntity = 0; + m_nPlayerSlot = 0; + m_bSignonState = false; + m_flStartTime = 0; + m_flFPS = 0; + m_nGameServerMaxClients = 0; + m_fNextSendUpdateTime = 0; + Q_memset( m_pRecvTables, 0, sizeof( m_pRecvTables ) ); + m_nRecvTables = 0; + m_vPVSOrigin.Init(); + m_nStartTick = 0; + m_bPlayingBack = false; + m_bPlaybackPaused = false; + m_flPlaybackRateModifier = 0; + m_nSkipToTick = 0; + m_bMasterOnlyMode = false; + m_ClientState.m_pHLTV = this; + m_nGlobalSlots = 0; + m_nGlobalClients = 0; + m_nGlobalProxies = 0; +} + +CHLTVServer::~CHLTVServer() +{ + if ( m_nRecvTables > 0 ) + { + RecvTable_Term(); + FreeClientRecvTables(); + } + + // make sure everything was destroyed + Assert( m_CurrentFrame == NULL ); + Assert( CountClientFrames() == 0 ); +} + +void CHLTVServer::SetMaxClients( int number ) +{ + // allow max clients 0 in HLTV + m_nMaxclients = clamp( number, 0, ABSOLUTE_PLAYER_LIMIT ); +} + +void CHLTVServer::StartMaster(CGameClient *client) +{ + Clear(); // clear old settings & buffers + + if ( !client ) + { + ConMsg("SourceTV client not found.\n"); + return; + } + + m_Director = serverGameDirector; + + if ( !m_Director ) + { + ConMsg("Mod doesn't support SourceTV. No director module found.\n"); + return; + } + + m_MasterClient = client; + m_MasterClient->m_bIsHLTV = true; +#if defined( REPLAY_ENABLED ) + m_MasterClient->m_bIsReplay = false; +#endif + + // let game.dll know that we are the HLTV client + Assert( serverGameClients ); + + CPlayerState *player = serverGameClients->GetPlayerState( m_MasterClient->edict ); + player->hltv = true; + + m_Server = (CGameServer*)m_MasterClient->GetServer(); + + // set default user settings + m_MasterClient->m_ConVars->SetString( "name", tv_name.GetString() ); + m_MasterClient->m_ConVars->SetString( "cl_team", "1" ); + m_MasterClient->m_ConVars->SetString( "rate", "30000" ); + m_MasterClient->m_ConVars->SetString( "cl_updaterate", "22" ); + m_MasterClient->m_ConVars->SetString( "cl_interp_ratio", "1.0" ); + m_MasterClient->m_ConVars->SetString( "cl_predict", "0" ); + + m_nViewEntity = m_MasterClient->GetPlayerSlot() + 1; + m_nPlayerSlot = m_MasterClient->GetPlayerSlot(); + + // copy server settings from m_Server + + m_nGameServerMaxClients = m_Server->GetMaxClients(); // maxclients is different on proxy (128) + serverclasses = m_Server->serverclasses; + serverclassbits = m_Server->serverclassbits; + V_memcpy( worldmapMD5.bits, m_Server->worldmapMD5.bits, MD5_DIGEST_LENGTH ); + m_flTickInterval= m_Server->GetTickInterval(); + + // allocate buffers for input frame + m_HLTVFrame.AllocBuffers(); + + InstallStringTables(); + + // activate director in game.dll + m_Director->SetHLTVServer( this ); + + // register as listener for mod specific events + const char **modevents = m_Director->GetModEvents(); + + int j = 0; + while ( modevents[j] != NULL ) + { + const char *eventname = modevents[j]; + + CGameEventDescriptor *descriptor = g_GameEventManager.GetEventDescriptor( eventname ); + + if ( descriptor ) + { + g_GameEventManager.AddListener( this, descriptor, CGameEventManager::CLIENTSTUB ); + } + else + { + DevMsg("CHLTVServer::StartMaster: game event %s not found.\n", eventname ); + } + + j++; + } + + // copy signon buffers + m_Signon.StartWriting( m_Server->m_Signon.GetBasePointer(), m_Server->m_Signon.m_nDataBytes, + m_Server->m_Signon.GetNumBitsWritten() ); + + Q_strncpy( m_szMapname, m_Server->m_szMapname, sizeof(m_szMapname) ); + Q_strncpy( m_szSkyname, m_Server->m_szSkyname, sizeof(m_szSkyname) ); + + NET_ListenSocket( m_Socket, true ); // activated HLTV TCP socket + + m_MasterClient->ExecuteStringCommand( "spectate" ); // become a spectator + + m_MasterClient->UpdateUserSettings(); // make sure UserInfo is correct + + // hack reduce signontick by one to catch changes made in the current tick + m_MasterClient->m_nSignonTick--; + + if ( m_bMasterOnlyMode ) + { + // we allow only one client in master only mode + tv_maxclients.SetValue( min(1,tv_maxclients.GetInt()) ); + } + + SetMaxClients( tv_maxclients.GetInt() ); + + m_bSignonState = false; //master proxy is instantly connected + + m_nSpawnCount++; + + m_flStartTime = net_time; + + m_State = ss_active; + + // stop any previous recordings + m_DemoRecorder.StopRecording(); + + // start new recording if autorecord is enabled + if ( tv_autorecord.GetBool() ) + { + m_DemoRecorder.StartAutoRecording(); + } + + ReconnectClients(); +} + +void CHLTVServer::StartDemo(const char *filename) +{ + +} + +bool CHLTVServer::DispatchToRelay( CHLTVClient *pClient ) +{ + if ( tv_dispatchmode.GetInt() <= DISPATCH_MODE_OFF ) + return false; // don't redirect + + CBaseClient *pBestProxy = NULL; + float fBestRatio = 1.0f; + + // find best relay proxy + for (int i=0; i < GetClientCount(); i++ ) + { + CBaseClient *pProxy = m_Clients[ i ]; + + // check all known proxies + if ( !pProxy->IsConnected() || !pProxy->IsHLTV() || (pClient == pProxy) ) + continue; + + int slots = Q_atoi( pProxy->GetUserSetting( "hltv_slots" ) ); + int clients = Q_atoi( pProxy->GetUserSetting( "hltv_clients" ) ); + + // skip overloaded proxies or proxies with no slots at all + if ( (clients > slots) || slots <= 0 ) + continue; + + // calc clients/slots ratio for this proxy + float ratio = ((float)(clients))/((float)slots); + + if ( ratio < fBestRatio ) + { + fBestRatio = ratio; + pBestProxy = pProxy; + } + } + + if ( pBestProxy == NULL ) + { + if ( tv_dispatchmode.GetInt() == DISPATCH_MODE_ALWAYS ) + { + // we are in always forward mode, drop client if we can't forward it + pClient->Disconnect("No SourceTV relay available"); + return true; + } + else + { + // just let client connect to this proxy + return false; + } + } + + // check if client should stay on this relay server + if ( (tv_dispatchmode.GetInt() == DISPATCH_MODE_AUTO) && (GetMaxClients() > 0) ) + { + // ratio = clients/slots. give relay proxies 25% bonus + float myRatio = ((float)GetNumClients()/(float)GetMaxClients()) * 1.25f; + + myRatio = min( myRatio, 1.0f ); // clamp to 1 + + // if we have a better local ratio then other proxies, keep this client here + if ( myRatio < fBestRatio ) + return false; // don't redirect + } + + const char *pszRelayAddr = pBestProxy->GetUserSetting("hltv_addr"); + + if ( !pszRelayAddr ) + return false; + + + ConMsg( "Redirecting spectator %s to SourceTV relay %s\n", + pClient->GetNetChannel()->GetRemoteAddress().ToString(), + pszRelayAddr ); + + // first tell that client that we are a SourceTV server, + // otherwise it's might ignore the command + SVC_ServerInfo serverInfo; + FillServerInfo( serverInfo ); + pClient->SendNetMsg( serverInfo, true ); + + // tell the client to connect to this new address + NET_StringCmd cmdMsg( va("redirect %s\n", pszRelayAddr ) ) ; + pClient->SendNetMsg( cmdMsg, true ); + + // increase this proxies client number in advance so this proxy isn't used again next time + int clients = Q_atoi( pBestProxy->GetUserSetting( "hltv_clients" ) ); + pBestProxy->SetUserCVar( "hltv_clients", va("%d", clients+1 ) ); + + return true; +} + +void CHLTVServer::ConnectRelay(const char *address) +{ + if ( m_ClientState.IsConnected() ) + { + // do not try to reconnect to old connection + m_ClientState.m_szRetryAddress[0] = 0; + + // disconnect first + m_ClientState.Disconnect( "HLTV server connecting to relay", true ); + + Changelevel(); // inactivate clients + } + + // connect to new server + m_ClientState.Connect( address, "tvrelay" ); +} + +void CHLTVServer::StartRelay() +{ + if ( !m_ClientState.IsConnected() && !IsPlayingBack() ) + { + DevMsg("StartRelay: not connected.\n"); + Shutdown(); + return; + } + + Clear(); // clear old settings & buffers + + if ( m_nRecvTables == 0 ) + { + // must be done only once since Mod never changes + InitClientRecvTables(); + } + + m_HLTVFrame.AllocBuffers(); + + m_StringTables = &m_NetworkStringTables; + + SetMaxClients( tv_maxclients.GetInt() ); + + m_bSignonState = true; + + m_flStartTime = net_time; + + m_State = ss_loading; + + m_nSpawnCount++; +} + +int CHLTVServer::GetHLTVSlot( void ) +{ + return m_nPlayerSlot; +} + +float CHLTVServer::GetOnlineTime( void ) +{ + return max(0., net_time - m_flStartTime); +} + +void CHLTVServer::GetLocalStats( int &proxies, int &slots, int &clients ) +{ + proxies = GetNumProxies(); + clients = GetNumClients(); + slots = GetMaxClients(); +} + +void CHLTVServer::GetRelayStats( int &proxies, int &slots, int &clients ) +{ + proxies = slots = clients = 0; + + for (int i=0 ; i < GetClientCount() ; i++ ) + { + CBaseClient *client = m_Clients[ i ]; + + if ( !client->IsConnected() || !client->IsHLTV() ) + continue; + + proxies += Q_atoi( client->GetUserSetting( "hltv_proxies" ) ); + slots += Q_atoi( client->GetUserSetting( "hltv_slots" ) ); + clients += Q_atoi( client->GetUserSetting( "hltv_clients" ) ); + } +} + +void CHLTVServer::GetGlobalStats( int &proxies, int &slots, int &clients ) +{ + // the master proxy is the only one that really has all data to generate + // global stats + if ( IsMasterProxy() ) + { + GetRelayStats( m_nGlobalProxies, m_nGlobalSlots, m_nGlobalClients ); + + m_nGlobalSlots += GetMaxClients(); + m_nGlobalClients += GetNumClients(); + } + + // if this is a relay proxies, global data comes via the + // wire from the master proxy + proxies = m_nGlobalProxies; + slots = m_nGlobalSlots; + clients = m_nGlobalClients; +} + +const netadr_t *CHLTVServer::GetRelayAddress( void ) +{ + if ( IsMasterProxy() ) + { + return &net_local_adr; // TODO wrong port + } + else if ( m_ClientState.m_NetChannel ) + { + return &m_ClientState.m_NetChannel->GetRemoteAddress(); + } + else + { + return NULL; + } +} + +bool CHLTVServer::IsMasterProxy( void ) +{ + return ( m_MasterClient != NULL ); +} + +bool CHLTVServer::IsTVRelay() +{ + return !IsMasterProxy(); +} + +bool CHLTVServer::IsDemoPlayback( void ) +{ + return false; +} + +void CHLTVServer::BroadcastLocalTitle( CHLTVClient *client ) +{ + IGameEvent *event = g_GameEventManager.CreateEvent( "hltv_title", true ); + + if ( !event ) + return; + + event->SetString( "text", tv_title.GetString() ); + + char buffer_data[MAX_EVENT_BYTES]; + + SVC_GameEvent eventMsg; + + eventMsg.SetReliable( true ); + + eventMsg.m_DataOut.StartWriting( buffer_data, sizeof(buffer_data) ); + + // create bit stream from KeyValues + if ( !g_GameEventManager.SerializeEvent( event, &eventMsg.m_DataOut ) ) + { + DevMsg("CHLTVServer: failed to serialize title '%s'.\n", event->GetName() ); + g_GameEventManager.FreeEvent( event ); + return; + } + + if ( client ) + { + client->SendNetMsg( eventMsg ); + } + else + { + for ( int i = 0; i < m_Clients.Count(); i++ ) + { + client = Client(i); + + if ( !client->IsActive() || client->IsHLTV() ) + continue; + + client->SendNetMsg( eventMsg ); + } + } + + g_GameEventManager.FreeEvent( event ); +} + +void CHLTVServer::BroadcastLocalChat( const char *pszChat, const char *pszGroup ) +{ + IGameEvent *event = g_GameEventManager.CreateEvent( "hltv_chat", true ); + + if ( !event ) + return; + + event->SetString( "text", pszChat ); + + char buffer_data[MAX_EVENT_BYTES]; + + SVC_GameEvent eventMsg; + + eventMsg.SetReliable( false ); + + eventMsg.m_DataOut.StartWriting( buffer_data, sizeof(buffer_data) ); + + // create bit stream from KeyValues + if ( !g_GameEventManager.SerializeEvent( event, &eventMsg.m_DataOut ) ) + { + DevMsg("CHLTVServer: failed to serialize chat '%s'.\n", event->GetName() ); + g_GameEventManager.FreeEvent( event ); + return; + } + + for ( int i = 0; i < m_Clients.Count(); i++ ) + { + CHLTVClient *cl = Client(i); + + if ( !cl->IsActive() || !cl->IsSpawned() || cl->IsHLTV() ) + continue; + + // if this is a spectator chat message and client disabled it, don't show it + if ( Q_strcmp( cl->m_szChatGroup, pszGroup) || cl->m_bNoChat ) + continue; + + cl->SendNetMsg( eventMsg ); + } + + g_GameEventManager.FreeEvent( event ); +} + +void CHLTVServer::BroadcastEventLocal( IGameEvent *event, bool bReliable ) +{ + char buffer_data[MAX_EVENT_BYTES]; + + SVC_GameEvent eventMsg; + + eventMsg.SetReliable( bReliable ); + + eventMsg.m_DataOut.StartWriting( buffer_data, sizeof(buffer_data) ); + + // create bit stream from KeyValues + if ( !g_GameEventManager.SerializeEvent( event, &eventMsg.m_DataOut ) ) + { + DevMsg("CHLTVServer: failed to serialize local event '%s'.\n", event->GetName() ); + return; + } + + for ( int i = 0; i < m_Clients.Count(); i++ ) + { + CHLTVClient *cl = Client(i); + + if ( !cl->IsActive() || !cl->IsSpawned() || cl->IsHLTV() ) + continue; + + if ( !cl->SendNetMsg( eventMsg ) ) + { + if ( eventMsg.IsReliable() ) + { + DevMsg( "BroadcastMessage: Reliable broadcast message overflow for client %s", cl->GetClientName() ); + } + } + } + + if ( tv_debug.GetBool() ) + Msg("SourceTV broadcast local event: %s\n", event->GetName() ); +} + +void CHLTVServer::BroadcastEvent(IGameEvent *event) +{ + char buffer_data[MAX_EVENT_BYTES]; + + SVC_GameEvent eventMsg; + + eventMsg.m_DataOut.StartWriting( buffer_data, sizeof(buffer_data) ); + + // create bit stream from KeyValues + if ( !g_GameEventManager.SerializeEvent( event, &eventMsg.m_DataOut ) ) + { + DevMsg("CHLTVServer: failed to serialize event '%s'.\n", event->GetName() ); + return; + } + + BroadcastMessage( eventMsg, true, true ); + + if ( tv_debug.GetBool() ) + Msg("SourceTV broadcast event: %s\n", event->GetName() ); +} + +void CHLTVServer::FireGameEvent(IGameEvent *event) +{ + if ( !IsActive() ) + return; + + char buffer_data[MAX_EVENT_BYTES]; + + SVC_GameEvent eventMsg; + + eventMsg.m_DataOut.StartWriting( buffer_data, sizeof(buffer_data) ); + + // create bit stream from KeyValues + if ( g_GameEventManager.SerializeEvent( event, &eventMsg.m_DataOut ) ) + { + SendNetMsg( eventMsg ); + } + else + { + DevMsg("CHLTVServer::FireGameEvent: failed to serialize event '%s'.\n", event->GetName() ); + } +} + +bool CHLTVServer::ShouldUpdateMasterServer() +{ + + // If the main game server is active, then we let it update Steam with the server info. + return !sv.IsActive(); +} + +CBaseClient *CHLTVServer::CreateNewClient(int slot ) +{ + return new CHLTVClient( slot, this ); +} + +void CHLTVServer::InstallStringTables( void ) +{ +#ifndef SHARED_NET_STRING_TABLES + + int numTables = m_Server->m_StringTables->GetNumTables(); + + m_StringTables = &m_NetworkStringTables; + + Assert( m_StringTables->GetNumTables() == 0); // must be empty + + m_StringTables->AllowCreation( true ); + + // master hltv needs to keep a list of changes for all table items + m_StringTables->EnableRollback( true ); + + for ( int i =0; i<numTables; i++) + { + // iterate through server tables + CNetworkStringTable *serverTable = + (CNetworkStringTable*)m_Server->m_StringTables->GetTable( i ); + + if ( !serverTable ) + continue; + + // get matching client table + CNetworkStringTable *hltvTable = + (CNetworkStringTable*)m_StringTables->CreateStringTableEx( + serverTable->GetTableName(), + serverTable->GetMaxStrings(), + serverTable->GetUserDataSize(), + serverTable->GetUserDataSizeBits(), + serverTable->HasFileNameStrings() + ); + + if ( !hltvTable ) + { + DevMsg("SV_InstallHLTVStringTableMirrors! Missing client table \"%s\".\n ", serverTable->GetTableName() ); + continue; + } + + // make hltv table an exact copy of server table + hltvTable->CopyStringTable( serverTable ); + + // link hltv table to server table + serverTable->SetMirrorTable( hltvTable ); + } + + m_StringTables->AllowCreation( false ); + +#endif +} + +void CHLTVServer::RestoreTick( int tick ) +{ +#ifndef SHARED_NET_STRING_TABLES + + // only master proxy delays time + if ( !IsMasterProxy() ) + return; + + int numTables = m_StringTables->GetNumTables(); + + for ( int i =0; i<numTables; i++) + { + // iterate through server tables + CNetworkStringTable *pTable = (CNetworkStringTable*) m_StringTables->GetTable( i ); + pTable->RestoreTick( tick ); + } + +#endif +} + +void CHLTVServer::UserInfoChanged( int nClientIndex ) +{ + // don't change UserInfo table, it keeps the infos of the original players +} + +void CHLTVServer::LinkInstanceBaselines( void ) +{ + // Forces to update m_pInstanceBaselineTable. + AUTO_LOCK( g_svInstanceBaselineMutex ); + GetInstanceBaselineTable(); + + Assert( m_pInstanceBaselineTable ); + + // update all found server classes + for ( ServerClass *pClass = serverGameDLL->GetAllServerClasses(); pClass; pClass=pClass->m_pNext ) + { + char idString[32]; + Q_snprintf( idString, sizeof( idString ), "%d", pClass->m_ClassID ); + + // Ok, make a new instance baseline so they can reference it. + int index = m_pInstanceBaselineTable->FindStringIndex( idString ); + + if ( index != -1 ) + { + pClass->m_InstanceBaselineIndex = index; + } + else + { + pClass->m_InstanceBaselineIndex = INVALID_STRING_INDEX; + } + } +} + +/* CHLTVServer::GetOriginFromPackedEntity is such a bad, bad hack. + +extern float DecodeFloat(SendProp const *pProp, bf_read *pIn); + +Vector CHLTVServer::GetOriginFromPackedEntity(PackedEntity* pe) +{ + Vector origin; origin.Init(); + + SendTable *pSendTable = pe->m_pSendTable; + + // recursively go down until BaseEntity sendtable + while ( Q_strcmp( pSendTable->GetName(), "DT_BaseEntity") ) + { + SendProp *pProp = pSendTable->GetProp( 0 ); // 0 = baseclass + pSendTable = pProp->GetDataTable(); + } + + for ( int i=0; i < pSendTable->GetNumProps(); i++ ) + { + SendProp *pProp = pSendTable->GetProp( i ); + + if ( Q_strcmp( pProp->GetName(), "m_vecOrigin" ) == 0 ) + { + Assert( pProp->GetType() == DPT_Vector ); + + bf_read buf( pe->LockData(), Bits2Bytes(pe->GetNumBits()), pProp->GetOffset() ); + + origin[0] = DecodeFloat(pProp, &buf); + origin[1] = DecodeFloat(pProp, &buf); + origin[2] = DecodeFloat(pProp, &buf); + + break; + } + } + + return origin; +} */ + +CHLTVEntityData *FindHLTVDataInSnapshot( CFrameSnapshot * pSnapshot, int iEntIndex ) +{ + int a = 0; + int z = pSnapshot->m_nValidEntities-1; + + if ( iEntIndex < pSnapshot->m_pValidEntities[a] || + iEntIndex > pSnapshot->m_pValidEntities[z] ) + return NULL; + + while ( a < z ) + { + int m = (a+z)/2; + + int index = pSnapshot->m_pValidEntities[m]; + + if ( index == iEntIndex ) + return &pSnapshot->m_pHLTVEntityData[m]; + + if ( iEntIndex > index ) + { + if ( pSnapshot->m_pValidEntities[z] == iEntIndex ) + return &pSnapshot->m_pHLTVEntityData[z]; + + if ( a == m ) + return NULL; + + a = m; + } + else + { + if ( pSnapshot->m_pValidEntities[a] == iEntIndex ) + return &pSnapshot->m_pHLTVEntityData[a]; + + if ( z == m ) + return NULL; + + z = m; + } + } + + return NULL; +} + +void CHLTVServer::EntityPVSCheck( CClientFrame *pFrame ) +{ + byte PVS[PAD_NUMBER( MAX_MAP_CLUSTERS,8 ) / 8]; + int nPVSSize = (GetCollisionBSPData()->numclusters + 7) / 8; + + // setup engine PVS + SV_ResetPVS( PVS, nPVSSize ); + + CFrameSnapshot * pSnapshot = pFrame->GetSnapshot(); + + Assert ( pSnapshot->m_pHLTVEntityData != NULL ); + + int nDirectorEntity = m_Director->GetPVSEntity(); + + if ( pSnapshot && nDirectorEntity > 0 ) + { + CHLTVEntityData *pHLTVData = FindHLTVDataInSnapshot( pSnapshot, nDirectorEntity ); + + if ( pHLTVData ) + { + m_vPVSOrigin.x = pHLTVData->origin[0]; + m_vPVSOrigin.y = pHLTVData->origin[1]; + m_vPVSOrigin.z = pHLTVData->origin[2]; + } + } + else + { + m_vPVSOrigin = m_Director->GetPVSOrigin(); + } + + + SV_AddOriginToPVS( m_vPVSOrigin ); + + // know remove all entities that aren't in PVS + int entindex = -1; + + while ( true ) + { + entindex = pFrame->transmit_entity.FindNextSetBit( entindex+1 ); + + if ( entindex < 0 ) + break; + + // is transmit_always is set -> no PVS check + if ( pFrame->transmit_always->Get(entindex) ) + { + pFrame->last_entity = entindex; + continue; + } + + CHLTVEntityData *pHLTVData = FindHLTVDataInSnapshot( pSnapshot, entindex ); + + if ( !pHLTVData ) + continue; + + unsigned int nNodeCluster = pHLTVData->m_nNodeCluster; + + // check if node or cluster is in PVS + + if ( nNodeCluster & (1<<31) ) + { + // it's a node SLOW + nNodeCluster &= ~(1<<31); + if ( CM_HeadnodeVisible( nNodeCluster, PVS, nPVSSize ) ) + { + pFrame->last_entity = entindex; + continue; + } + } + else + { + // it's a cluster QUICK + if ( PVS[nNodeCluster >> 3] & (1 << (nNodeCluster & 7)) ) + { + pFrame->last_entity = entindex; + continue; + } + } + + // entity is not in PVS, remove from transmit_entity list + pFrame->transmit_entity.Clear( entindex ); + } +} + +void CHLTVServer::SignonComplete() +{ + Assert ( !IsMasterProxy() ); + + m_bSignonState = false; + + LinkInstanceBaselines(); + + if ( tv_debug.GetBool() ) + Msg("SourceTV signon complete.\n" ); +} + +CClientFrame *CHLTVServer::AddNewFrame( CClientFrame *clientFrame ) +{ + VPROF_BUDGET( "CHLTVServer::AddNewFrame", "HLTV" ); + + Assert ( clientFrame ); + Assert( clientFrame->tick_count > m_nLastTick ); + + m_nLastTick = clientFrame->tick_count; + + m_HLTVFrame.SetSnapshot( clientFrame->GetSnapshot() ); + m_HLTVFrame.tick_count = clientFrame->tick_count; + m_HLTVFrame.last_entity = clientFrame->last_entity; + m_HLTVFrame.transmit_entity = clientFrame->transmit_entity; + + // remember tick of first valid frame + if ( m_nFirstTick < 0 ) + { + m_nFirstTick = clientFrame->tick_count; + m_nTickCount = m_nFirstTick; + + if ( !IsMasterProxy() ) + { + Assert ( m_State == ss_loading ); + m_State = ss_active; // we are now ready to go + + ReconnectClients(); + + ConMsg("SourceTV relay active.\n" ); + + Steam3Server().Activate( CSteam3Server::eServerTypeTVRelay ); + Steam3Server().SendUpdatedServerDetails(); + } + else + { + ConMsg("SourceTV broadcast active.\n" ); + } + } + + CHLTVFrame *hltvFrame = new CHLTVFrame; + + // copy tickcount & entities from client frame + hltvFrame->CopyFrame( *clientFrame ); + + //copy rest (messages, tempents) from current HLTV frame + hltvFrame->CopyHLTVData( m_HLTVFrame ); + + // add frame to HLTV server + AddClientFrame( hltvFrame ); + + if ( IsMasterProxy() && m_DemoRecorder.IsRecording() ) + { + m_DemoRecorder.WriteFrame( &m_HLTVFrame ); + } + + // reset HLTV frame for recording next messages etc. + m_HLTVFrame.Reset(); + m_HLTVFrame.SetSnapshot( NULL ); + + return hltvFrame; +} + +void CHLTVServer::SendClientMessages ( bool bSendSnapshots ) +{ + // build individual updates + for ( int i=0; i< m_Clients.Count(); i++ ) + { + CHLTVClient* client = Client(i); + + // Update Host client send state... + if ( !client->ShouldSendMessages() ) + { + continue; + } + + // Append the unreliable data (player updates and packet entities) + if ( m_CurrentFrame && client->IsActive() ) + { + // don't send same snapshot twice + client->SendSnapshot( m_CurrentFrame ); + } + else + { + // Connected, but inactive, just send reliable, sequenced info. + client->m_NetChannel->Transmit(); + } + + client->UpdateSendState(); + client->m_fLastSendTime = net_time; + } +} + +void CHLTVServer::UpdateStats( void ) +{ + if ( m_fNextSendUpdateTime > net_time ) + return; + + m_fNextSendUpdateTime = net_time + 8.0f; + + // fire game event for everyone + IGameEvent *event = NULL; + + if ( !IsMasterProxy() && !m_ClientState.IsConnected() ) + { + // we are disconnected from SourceTV server + event = g_GameEventManager.CreateEvent( "hltv_message", true ); + + if ( !event ) + return; + + event->SetString( "text", "SourceTV reconnecting ..." ); + } + else + { + int proxies, slots, clients; + GetGlobalStats( proxies, slots, clients ); + + event = g_GameEventManager.CreateEvent( "hltv_status", true ); + + if ( !event ) + return; + + char address[32]; + + if ( IsMasterProxy() || tv_overridemaster.GetBool() ) + { + // broadcast own address + Q_snprintf( address, sizeof(address), "%s:%u", net_local_adr.ToString(true), GetUDPPort() ); + } + else + { + // forward address + Q_snprintf( address, sizeof(address), "%s", m_RootServer.ToString() ); + } + + event->SetString( "master", address ); + event->SetInt( "clients", clients ); + event->SetInt( "slots", slots); + event->SetInt( "proxies", proxies ); + } + + if ( IsMasterProxy() ) + { + // as a master fire event for every one + g_GameEventManager.FireEvent( event ); + } + else + { + // as a relay proxy just broadcast event + BroadcastEvent( event ); + } + +} + +bool CHLTVServer::SendNetMsg( INetMessage &msg, bool bForceReliable ) +{ + if ( m_bSignonState ) + { + return msg.WriteToBuffer( m_Signon ); + } + + int buffer = HLTV_BUFFER_UNRELIABLE; // default destination + + if ( msg.IsReliable() ) + { + buffer = HLTV_BUFFER_RELIABLE; + } + else if ( msg.GetType() == svc_Sounds ) + { + buffer = HLTV_BUFFER_SOUNDS; + } + else if ( msg.GetType() == svc_VoiceData ) + { + buffer = HLTV_BUFFER_VOICE; + } + else if ( msg.GetType() == svc_TempEntities ) + { + buffer = HLTV_BUFFER_TEMPENTS; + } + + // anything else goes to the unreliable bin + return msg.WriteToBuffer( m_HLTVFrame.m_Messages[buffer] ); +} + +bf_write *CHLTVServer::GetBuffer( int nBuffer ) +{ + if ( nBuffer < 0 || nBuffer >= HLTV_BUFFER_MAX ) + return NULL; + + return &m_HLTVFrame.m_Messages[nBuffer]; +} + +IServer *CHLTVServer::GetBaseServer() +{ + return (IServer*)this; +} + +IHLTVDirector *CHLTVServer::GetDirector() +{ + return m_Director; +} + +CClientFrame *CHLTVServer::GetDeltaFrame( int nTick ) +{ + if ( !tv_deltacache.GetBool() ) + return GetClientFrame( nTick ); //expensive + + // TODO make that a utlmap + FOR_EACH_VEC( m_FrameCache, iFrame ) + { + if ( m_FrameCache[iFrame].nTick == nTick ) + return m_FrameCache[iFrame].pFrame; + } + + int i = m_FrameCache.AddToTail(); + + CFrameCacheEntry_s &entry = m_FrameCache[i]; + + entry.nTick = nTick; + entry.pFrame = GetClientFrame( nTick ); //expensive + + return entry.pFrame; +} + +void CHLTVServer::RunFrame() +{ + VPROF_BUDGET( "CHLTVServer::RunFrame", "HLTV" ); + + // update network time etc + NET_RunFrame( Plat_FloatTime() ); + + if ( m_ClientState.m_nSignonState > SIGNONSTATE_NONE ) + { + // process data from net socket + NET_ProcessSocket( m_ClientState.m_Socket, &m_ClientState ); + + m_ClientState.RunFrame(); + + m_ClientState.SendPacket(); + } + + // check if HLTV server if active + if ( !IsActive() ) + return; + + if ( host_frametime > 0 ) + { + m_flFPS = m_flFPS * 0.99f + 0.01f/host_frametime; + } + + if ( IsPlayingBack() ) + return; + + // get current tick time for director module and restore + // world (stringtables, framebuffers) as they were at this time + UpdateTick(); + + // Run any commands from client and play client Think functions if it is time. + CBaseServer::RunFrame(); + + UpdateStats(); + + SendClientMessages( true ); + + // Update the Steam server if we're running a relay. + if ( !sv.IsActive() ) + Steam3Server().RunFrame(); + + UpdateMasterServer(); +} + +void CHLTVServer::UpdateTick( void ) +{ + VPROF_BUDGET( "CHLTVServer::UpdateTick", "HLTV" ); + + if ( m_nFirstTick < 0 ) + { + m_nTickCount = 0; + m_CurrentFrame = NULL; + return; + } + + // set tick time to last frame added + int nNewTick = m_nLastTick; + + if ( IsMasterProxy() ) + { + // get tick from director, he decides delay etc + nNewTick = max( m_nFirstTick, m_Director->GetDirectorTick() ); + } + + // the the closest available frame + CHLTVFrame *newFrame = (CHLTVFrame*) GetClientFrame( nNewTick, false ); + + if ( newFrame == NULL ) + return; // we dont have a new frame + + if ( m_CurrentFrame == newFrame ) + return; // current frame didn't change + + m_CurrentFrame = newFrame; + m_nTickCount = m_CurrentFrame->tick_count; + + if ( IsMasterProxy() ) + { + // now do master proxy stuff + + // restore string tables for this time + RestoreTick( m_nTickCount ); + + // remove entities out of current PVS + if ( tv_transmitall.GetBool() == false ) + { + EntityPVSCheck( m_CurrentFrame ); + } + } + else + { + // delta entity cache works only for relay proxies + m_DeltaCache.SetTick( m_CurrentFrame->tick_count, m_CurrentFrame->last_entity+1 ); + } + + int removeTick = m_nTickCount - 16.0f/m_flTickInterval; // keep 16 seconds buffer + + if ( removeTick > 0 ) + { + DeleteClientFrames( removeTick ); + } + + m_FrameCache.RemoveAll(); +} + +const char *CHLTVServer::GetName( void ) const +{ + return tv_name.GetString(); +} + +void CHLTVServer::FillServerInfo(SVC_ServerInfo &serverinfo) +{ + CBaseServer::FillServerInfo( serverinfo ); + + serverinfo.m_nPlayerSlot = m_nPlayerSlot; // all spectators think they're the HLTV client + serverinfo.m_nMaxClients = m_nGameServerMaxClients; +} + +void CHLTVServer::Clear( void ) +{ + CBaseServer::Clear(); + + m_Director = NULL; + m_MasterClient = NULL; + m_ClientState.Clear(); + m_Server = NULL; + m_nFirstTick = -1; + m_nLastTick = 0; + m_nTickCount = 0; + m_CurrentFrame = NULL; + m_nPlayerSlot = 0; + m_flStartTime = 0.0f; + m_nViewEntity = 1; + m_nGameServerMaxClients = 0; + m_fNextSendUpdateTime = 0.0f; + m_HLTVFrame.FreeBuffers(); + m_vPVSOrigin.Init(); + + DeleteClientFrames( -1 ); + + m_DeltaCache.Flush(); + m_FrameCache.RemoveAll(); +} + +bool CHLTVServer::ProcessConnectionlessPacket( netpacket_t * packet ) +{ + bf_read msg = packet->message; // We're copying the message, so we don't need to seek back when passing packet to the base class. +// int bits = msg.GetNumBitsRead(); + + char c = msg.ReadChar(); + + if ( c == 0 ) + { + return false; + } + + switch ( c ) + { +#ifndef NO_STEAM + case A2S_INFO: + char rgchInfoPostfix[64]; + msg.ReadString( rgchInfoPostfix, sizeof( rgchInfoPostfix ) ); + if ( !Q_stricmp( rgchInfoPostfix, A2S_KEY_STRING_STEAM ) ) + { + ReplyInfo( packet->from ); + return true; + } + + break; + //case A2S_PLAYER: + // return true; +#endif // #ifndef NO_STEAM + } + + return CBaseServer::ProcessConnectionlessPacket( packet ); +} + +void CHLTVServer::Init(bool bIsDedicated) +{ + CBaseServer::Init( bIsDedicated ); + + m_Socket = NS_HLTV; + + // check if only master proxy is allowed, no broadcasting + if ( CommandLine()->FindParm("-tvmasteronly") ) + { + m_bMasterOnlyMode = true; + } +} + +void CHLTVServer::Changelevel() +{ + m_DemoRecorder.StopRecording(); + + InactivateClients(); + + DeleteClientFrames(-1); + + m_CurrentFrame = NULL; +} + +void CHLTVServer::GetNetStats( float &avgIn, float &avgOut ) +{ + CBaseServer::GetNetStats( avgIn, avgOut ); + + if ( m_ClientState.IsActive() ) + { + avgIn += m_ClientState.m_NetChannel->GetAvgData(FLOW_INCOMING); + avgOut += m_ClientState.m_NetChannel->GetAvgData(FLOW_OUTGOING); + } +} + +void CHLTVServer::Shutdown( void ) +{ + m_DemoRecorder.StopRecording(); // if recording, stop now + + if ( IsMasterProxy() ) + { + if ( m_MasterClient ) + m_MasterClient->Disconnect( "SourceTV stop." ); + + if ( m_Director ) + m_Director->SetHLTVServer( NULL ); + } + else + { + // do not try to reconnect to old connection + m_ClientState.m_szRetryAddress[0] = 0; + + m_ClientState.Disconnect( "HLTV server shutting down", true ); + } + + g_GameEventManager.RemoveListener( this ); + + CBaseServer::Shutdown(); +} + +CDemoFile *CHLTVServer::GetDemoFile() +{ + return &m_DemoFile; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CHLTVServer::IsPlayingBack( void ) +{ + return m_bPlayingBack; +} + +bool CHLTVServer::IsPlaybackPaused() +{ + return m_bPlaybackPaused; +} + +float CHLTVServer::GetPlaybackTimeScale() +{ + return m_flPlaybackRateModifier; +} + +void CHLTVServer::SetPlaybackTimeScale(float timescale) +{ + m_flPlaybackRateModifier = timescale; +} + +void CHLTVServer::ResyncDemoClock() +{ + m_nStartTick = host_tickcount; +} + +int CHLTVServer::GetPlaybackStartTick( void ) +{ + return m_nStartTick; +} + +int CHLTVServer::GetPlaybackTick( void ) +{ + return host_tickcount - m_nStartTick; +} + +int CHLTVServer::GetTotalTicks(void) +{ + return m_DemoFile.m_DemoHeader.playback_ticks; +} + +bool CHLTVServer::StartPlayback( const char *filename, bool bAsTimeDemo ) +{ + Clear(); + + if ( !m_DemoFile.Open( filename, true ) ) + { + return false; + } + + // Read in the m_DemoHeader + demoheader_t *dh = m_DemoFile.ReadDemoHeader(); + + if ( !dh ) + { + ConMsg( "Failed to read demo header.\n" ); + m_DemoFile.Close(); + return false; + } + + // create a fake channel with a NULL address + m_ClientState.m_NetChannel = NET_CreateNetChannel( NS_CLIENT, NULL, "DEMO", &m_ClientState ); + + if ( !m_ClientState.m_NetChannel ) + { + ConMsg( "CDemo::Play: failed to create demo net channel\n" ); + m_DemoFile.Close(); + return false; + } + + m_ClientState.m_NetChannel->SetTimeout( -1.0f ); // never timeout + + + // Now read in the directory structure. + + m_bPlayingBack = true; + + ConMsg( "Reading complete demo file at once...\n"); + + double start = Plat_FloatTime(); + + ReadCompleteDemoFile(); + + double diff = Plat_FloatTime() - start; + + ConMsg( "Reading time :%.4f\n", diff ); + + NET_RemoveNetChannel( m_ClientState.m_NetChannel, true ); + m_ClientState.m_NetChannel = NULL; + + return true; +} + +void CHLTVServer::ReadCompleteDemoFile() +{ + int tick = 0; + byte cmd = dem_signon; + char buffer[NET_MAX_PAYLOAD]; + netpacket_t demoPacket; + + // setup demo packet data buffer + Q_memset( &demoPacket, 0, sizeof(demoPacket) ); + demoPacket.from.SetType( NA_LOOPBACK); + + while ( true ) + { + m_DemoFile.ReadCmdHeader( cmd, tick ); + + // COMMAND HANDLERS + switch ( cmd ) + { + case dem_synctick: + ResyncDemoClock(); + break; + case dem_stop: + // MOTODO we finished reading the file + return ; + case dem_consolecmd: + { + NET_StringCmd cmdmsg( m_DemoFile.ReadConsoleCommand() ); + m_ClientState.ProcessStringCmd( &cmdmsg ); + } + break; + case dem_datatables: + { + ALIGN4 char data[64*1024] ALIGN4_POST; + bf_read buf( "dem_datatables", data, sizeof(data) ); + + m_DemoFile.ReadNetworkDataTables( &buf ); + buf.Seek( 0 ); + + // support for older engine demos + if ( !DataTable_LoadDataTablesFromBuffer( &buf, m_DemoFile.m_DemoHeader.demoprotocol ) ) + { + Host_Error( "Error parsing network data tables during demo playback." ); + } + } + break; + case dem_stringtables: + { + void *data = NULL; + int dataLen = 512 * 1024; + while ( dataLen <= DEMO_FILE_MAX_STRINGTABLE_SIZE ) + { + data = realloc( data, dataLen ); + bf_read buf( "dem_stringtables", data, dataLen ); + // did we successfully read + if ( m_DemoFile.ReadStringTables( &buf ) > 0 ) + { + buf.Seek( 0 ); + if ( !networkStringTableContainerClient->ReadStringTables( buf ) ) + { + Host_Error( "Error parsing string tables during demo playback." ); + } + break; + } + + // Didn't fit. Try doubling the size of the buffer + dataLen *= 2; + } + + if ( dataLen > DEMO_FILE_MAX_STRINGTABLE_SIZE ) + { + Warning( "ReadCompleteDemoFile failed to read string tables. Trying to read string tables that's bigger than max string table size\n" ); + } + + free( data ); + } + break; + case dem_usercmd: + { + char bufferIn[256]; + int length = sizeof( bufferIn ); + m_DemoFile.ReadUserCmd( bufferIn, length ); + // MOTODO HLTV must store user commands too + } + break; + case dem_signon: + case dem_packet: + { + int inseq, outseqack = 0; + + m_DemoFile.ReadCmdInfo( m_LastCmdInfo ); // MOTODO must be stored somewhere + m_DemoFile.ReadSequenceInfo( inseq, outseqack ); + + int length = m_DemoFile.ReadRawData( buffer, sizeof(buffer) ); + + if ( length > 0 ) + { + // succsessfully read new demopacket + demoPacket.received = realtime; + demoPacket.size = length; + demoPacket.message.StartReading( buffer, length ); + + m_ClientState.m_NetChannel->ProcessPacket( &demoPacket, false ); + } + } + + break; + } + } +} + +int CHLTVServer::GetChallengeType ( netadr_t &adr ) +{ + return PROTOCOL_HASHEDCDKEY; // HLTV doesn't need Steam authentication +} + +const char *CHLTVServer::GetPassword() const +{ + const char *password = tv_password.GetString(); + + // if password is empty or "none", return NULL + if ( !password[0] || !Q_stricmp(password, "none" ) ) + { + return NULL; + } + + return password; +} + +IClient *CHLTVServer::ConnectClient ( netadr_t &adr, int protocol, int challenge, int clientChallenge, int authProtocol, + const char *name, const char *password, const char *hashedCDkey, int cdKeyLen ) +{ + IClient *client = (CHLTVClient*)CBaseServer::ConnectClient( + adr, protocol, challenge, clientChallenge, authProtocol, name, password, hashedCDkey, cdKeyLen ); + + if ( client ) + { + // remember password + CHLTVClient *pHltvClient = (CHLTVClient*)client; + Q_strncpy( pHltvClient->m_szPassword, password, sizeof(pHltvClient->m_szPassword) ); + } + + return client; +} + +int CHLTVServer::GetProtocolVersion() +{ + if ( GetDemoFile() ) + return GetDemoFile()->GetProtocolVersion(); + return PROTOCOL_VERSION; +} + +#ifndef NO_STEAM +void CHLTVServer::ReplyInfo( const netadr_t &adr ) +{ + static char gamedir[MAX_OSPATH]; + Q_FileBase( com_gamedir, gamedir, sizeof( gamedir ) ); + + CUtlBuffer buf; + buf.EnsureCapacity( 2048 ); + + buf.PutUnsignedInt( LittleDWord( CONNECTIONLESS_HEADER ) ); + buf.PutUnsignedChar( S2A_INFO_SRC ); + + buf.PutUnsignedChar( GetProtocolVersion() ); // Hardcoded protocol version number + buf.PutString( GetName() ); + buf.PutString( GetMapName() ); + buf.PutString( gamedir ); + buf.PutString( serverGameDLL->GetGameDescription() ); + + // The next field is a 16-bit version of the AppID. If our AppID < 65536, + // then let's go ahead and put in in there, to maximize compatibility + // with old clients who might be only using this field but not the new one. + // However, if our AppID won't fit, there's no way we can be compatible, + // anyway, so just put in a zero, which is better than a bogus AppID. + uint16 usAppIdShort = (uint16)GetSteamAppID(); + if ( (AppId_t)usAppIdShort != GetSteamAppID() ) + { + usAppIdShort = 0; + } + buf.PutShort( LittleWord( usAppIdShort ) ); + + // player info + buf.PutUnsignedChar( GetNumClients() ); + buf.PutUnsignedChar( GetMaxClients() ); + buf.PutUnsignedChar( 0 ); + + // NOTE: This key's meaning is changed in the new version. Since we send gameport and specport, + // it knows whether we're running SourceTV or not. Then it only needs to know if we're a dedicated or listen server. + if ( IsDedicated() ) + buf.PutUnsignedChar( 'd' ); // d = dedicated server + else + buf.PutUnsignedChar( 'l' ); // l = listen server + +#if defined(_WIN32) + buf.PutUnsignedChar( 'w' ); +#elif defined(OSX) + buf.PutUnsignedChar( 'm' ); +#else // LINUX? + buf.PutUnsignedChar( 'l' ); +#endif + + // Password? + buf.PutUnsignedChar( GetPassword() != NULL ? 1 : 0 ); + buf.PutUnsignedChar( Steam3Server().BSecure() ? 1 : 0 ); + buf.PutString( GetSteamInfIDVersionInfo().szVersionString ); + + // + // NEW DATA. + // + + // Write a byte with some flags that describe what is to follow. + const char *pchTags = sv_tags.GetString(); + byte nNewFlags = 0; + //if ( GetGamePort() != 0 ) + // nNewFlags |= S2A_EXTRA_DATA_HAS_GAME_PORT; + + if ( Steam3Server().GetGSSteamID().IsValid() ) + nNewFlags |= S2A_EXTRA_DATA_HAS_STEAMID; + + if ( GetUDPPort() != 0 ) + nNewFlags |= S2A_EXTRA_DATA_HAS_SPECTATOR_DATA; + + if ( pchTags && pchTags[0] != '\0' ) + nNewFlags |= S2A_EXTRA_DATA_HAS_GAMETAG_DATA; + + nNewFlags |= S2A_EXTRA_DATA_GAMEID; + + buf.PutUnsignedChar( nNewFlags ); + + // Write the rest of the data. + //if ( nNewFlags & S2A_EXTRA_DATA_HAS_GAME_PORT ) + //{ + // buf.PutShort( LittleWord( GetGamePort() ) ); + //} + + if ( nNewFlags & S2A_EXTRA_DATA_HAS_STEAMID ) + { + buf.PutUint64( LittleQWord( Steam3Server().GetGSSteamID().ConvertToUint64() ) ); + } + + if ( nNewFlags & S2A_EXTRA_DATA_HAS_SPECTATOR_DATA ) + { + buf.PutShort( LittleWord( GetUDPPort() ) ); + buf.PutString( GetName() ); + } + + if ( nNewFlags & S2A_EXTRA_DATA_HAS_GAMETAG_DATA ) + { + buf.PutString( pchTags ); + } + + if ( nNewFlags & S2A_EXTRA_DATA_GAMEID ) + { + // !FIXME! Is there a reason we aren't using the other half + // of this field? Shouldn't we put the game mod ID in there, too? + // We have the game dir. + buf.PutUint64( LittleQWord( CGameID( GetSteamAppID() ).ToUint64() ) ); + } + + NET_SendPacket( NULL, m_Socket, adr, (unsigned char *)buf.Base(), buf.TellPut() ); +} +#endif // #ifndef NO_STEAM + +CON_COMMAND( tv_status, "Show SourceTV server status." ) +{ + int slots, proxies, clients; + float in, out; + char gd[MAX_OSPATH]; + + Q_FileBase( com_gamedir, gd, sizeof( gd ) ); + + if ( !hltv || !hltv->IsActive() ) + { + ConMsg("SourceTV not active.\n" ); + return; + } + + hltv->GetNetStats( in, out ); + + in /= 1024; // as KB + out /= 1024; + + ConMsg("--- SourceTV Status ---\n"); + ConMsg("Online %s, FPS %.1f, Version %i (%s)\n", + COM_FormatSeconds( hltv->GetOnlineTime() ), hltv->m_flFPS, build_number(), + +#ifdef _WIN32 + "Win32" ); +#else + "Linux" ); +#endif + + if ( hltv->IsDemoPlayback() ) + { + ConMsg("Playing Demo File \"%s\"\n", "TODO demo file name" ); + } + else if ( hltv->IsMasterProxy() ) + { + ConMsg("Master \"%s\", delay %.0f\n", hltv->GetName(), hltv->GetDirector()->GetDelay() ); + } + else // if ( m_Server->IsRelayProxy() ) + { + if ( hltv->GetRelayAddress() ) + { + ConMsg("Relay \"%s\", connect to %s\n", hltv->GetName(), hltv->GetRelayAddress()->ToString() ); + } + else + { + ConMsg("Relay \"%s\", not connect.\n", hltv->GetName() ); + } + } + + ConMsg("Game Time %s, Mod \"%s\", Map \"%s\", Players %i\n", COM_FormatSeconds( hltv->GetTime() ), + gd, hltv->GetMapName(), hltv->GetNumPlayers() ); + + ConMsg("Local IP %s:%i, KB/sec In %.1f, Out %.1f\n", + net_local_adr.ToString( true ), hltv->GetUDPPort(), in ,out ); + + hltv->GetLocalStats( proxies, slots, clients ); + + ConMsg("Local Slots %i, Spectators %i, Proxies %i\n", + slots, clients-proxies, proxies ); + + hltv->GetGlobalStats( proxies, slots, clients); + + ConMsg("Total Slots %i, Spectators %i, Proxies %i\n", + slots, clients-proxies, proxies); + + if ( hltv->m_DemoRecorder.IsRecording() ) + { + ConMsg("Recording to \"%s\", length %s.\n", hltv->m_DemoRecorder.GetDemoFile()->m_szFileName, + COM_FormatSeconds( host_state.interval_per_tick * hltv->m_DemoRecorder.GetRecordingTick() ) ); + } +} + +CON_COMMAND( tv_relay, "Connect to SourceTV server and relay broadcast." ) +{ + if ( args.ArgC() < 2 ) + { + ConMsg( "Usage: tv_relay <ip:port>\n" ); + return; + } + + const char *address = args.ArgS(); + + // If it's not a single player connection to "localhost", initialize networking & stop listenserver + if ( !Q_strncmp( address, "localhost", 9 ) ) + { + ConMsg( "SourceTV can't connect to localhost.\n" ); + return; + } + + if ( !hltv ) + { + hltv = new CHLTVServer; + hltv->Init( NET_IsDedicated() ); + } + + if ( hltv->m_bMasterOnlyMode ) + { + ConMsg("SourceTV in Master-Only mode.\n" ); + return; + } + + // shutdown anything else + Host_Disconnect( false ); + + // start networking + NET_SetMutiplayer( true ); + + hltv->ConnectRelay( address ); +} + +CON_COMMAND( tv_stop, "Stops the SourceTV broadcast." ) +{ + if ( !hltv || !hltv->IsActive() ) + { + ConMsg("SourceTV not active.\n" ); + return; + } + + int nClients = hltv->GetNumClients(); + + hltv->Shutdown(); + + ConMsg("SourceTV stopped, %i clients disconnected.\n", nClients ); +} + +CON_COMMAND( tv_retry, "Reconnects the SourceTV relay proxy." ) +{ + if ( !hltv ) + { + ConMsg("SourceTV not active.\n" ); + return; + } + + if ( hltv->m_bMasterOnlyMode ) + { + ConMsg("SourceTV in Master-Only mode.\n" ); + return; + } + + if ( !hltv->m_ClientState.m_szRetryAddress[ 0 ] ) + { + ConMsg( "Can't retry, no previous SourceTV connection\n" ); + return; + } + + ConMsg( "Commencing SourceTV connection retry to %s\n", hltv->m_ClientState.m_szRetryAddress ); + Cbuf_AddText( va( "tv_relay %s\n", hltv->m_ClientState.m_szRetryAddress ) ); +} + +CON_COMMAND( tv_record, "Starts SourceTV demo recording." ) +{ + if ( args.ArgC() < 2 ) + { + ConMsg( "Usage: tv_record <filename>\n" ); + return; + } + + if ( !hltv || !hltv->IsActive() ) + { + ConMsg("SourceTV not active.\n" ); + return; + } + + if ( !hltv->IsMasterProxy() ) + { + ConMsg("Only SourceTV Master can record demos instantly.\n" ); + return; + } + + if ( hltv->m_DemoRecorder.IsRecording() ) + { + ConMsg("SourceTV already recording to %s.\n", hltv->m_DemoRecorder.GetDemoFile()->m_szFileName ); + return; + } + + // check path first + if ( !COM_IsValidPath( args[1] ) ) + { + ConMsg( "record %s: invalid path.\n", args[1] ); + return; + } + + char name[ MAX_OSPATH ]; + + Q_strncpy( name, args[1], sizeof( name ) ); + + // add .dem if not already set by user + Q_DefaultExtension( name, ".dem", sizeof( name ) ); + + hltv->m_DemoRecorder.StartRecording( name, false ); +} + +CON_COMMAND( tv_stoprecord, "Stops SourceTV demo recording." ) +{ + if ( !hltv || !hltv->IsActive() ) + { + ConMsg("SourceTV not active.\n" ); + return; + } + + hltv->m_DemoRecorder.StopRecording(); +} + +CON_COMMAND( tv_clients, "Shows list of connected SourceTV clients." ) +{ + if ( !hltv || !hltv->IsActive() ) + { + ConMsg("SourceTV not active.\n" ); + return; + } + + int nCount = 0; + + for ( int i=0; i<hltv->GetClientCount(); i++) + { + CHLTVClient *client = hltv->Client( i ); + INetChannel *netchan = client->GetNetChannel(); + + if ( !netchan ) + continue; + + ConMsg("ID: %i, \"%s\" %s, Time %s, %s, In %.1f, Out %.1f.\n", + client->GetUserID(), + client->GetClientName(), + client->IsHLTV() ? "(Relay)" : "", + COM_FormatSeconds( netchan->GetTimeConnected() ), + netchan->GetAddress(), + netchan->GetAvgData( FLOW_INCOMING ) / 1024, + netchan->GetAvgData( FLOW_OUTGOING ) / 1024 ); + + nCount++; + } + + ConMsg("--- Total %i connected clients ---\n", nCount ); +} + +CON_COMMAND( tv_msg, "Send a screen message to all clients." ) +{ + if ( !hltv || !hltv->IsActive() ) + { + ConMsg("SourceTV not active.\n" ); + return; + } + + IGameEvent *msg = g_GameEventManager.CreateEvent( "hltv_message", true ); + + if ( msg ) + { + msg->SetString( "text", args.ArgS() ); + hltv->BroadcastEventLocal( msg, false ); + g_GameEventManager.FreeEvent( msg ); + } +} + +#ifndef SWDS +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void EditDemo_f( const CCommand &args ) +{ + if ( cmd_source != src_command ) + return; + + if ( args.ArgC() < 2 ) + { + Msg ("editdemo <demoname> : edits a demo\n"); + return; + } + + // set current demo player to client demo player + demoplayer = hltv; + + // + // open the demo file + // + char name[ MAX_OSPATH ]; + + Q_strncpy( name, args[1], sizeof( name ) ); + + Q_DefaultExtension( name, ".dem", sizeof( name ) ); + + hltv->m_ClientState.m_bSaveMemory = true; + + demoplayer->StartPlayback( name, false ); +} + +CON_COMMAND_AUTOCOMPLETEFILE( editdemo, EditDemo_f, "Edit a recorded demo file (.dem ).", NULL, dem ); +#endif |