You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4059 lines
117 KiB
4059 lines
117 KiB
//========= Copyright 1996-2005, 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 <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 "dt_common_eng.h"
|
|
#include "baseautocompletefilelist.h"
|
|
#include "sv_steamauth.h"
|
|
#include "tier0/icommandline.h"
|
|
#include "mathlib/IceKey.H"
|
|
#include "tier1/fmtstr.h"
|
|
#include "serializedentity.h"
|
|
#include "changeframelist.h"
|
|
#include "ihltv.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
extern ConVar tv_snapshotrate, tv_snapshotrate1;
|
|
|
|
extern CNetworkStringTableContainer *networkStringTableContainerClient;
|
|
|
|
//we do not want our snapshot frames to be mixed in with the default snapshot frames
|
|
static const uint32 knHLTVSnapshotSet = CFrameSnapshotManager::knDefaultSnapshotSet + 1;
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// Construction/Destruction
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
CHLTVServer *g_pHltvServer[ HLTV_SERVER_MAX_COUNT ] = { NULL, NULL };
|
|
bool IsHltvActive()
|
|
{
|
|
CActiveHltvServerIterator hltv;
|
|
if ( hltv )
|
|
return true; // found at least one HLTV active
|
|
else
|
|
return false;
|
|
}
|
|
|
|
static void tv_title_changed_f( IConVar *var, const char *pOldString, float flOldValue )
|
|
{
|
|
for ( CActiveHltvServerIterator hltv; hltv; hltv.Next() )
|
|
{
|
|
hltv->BroadcastLocalTitle(); // broadcast hltv_title to the respective clients of this HLTV server
|
|
}
|
|
}
|
|
|
|
static void tv_name_changed_f( IConVar *var, const char *pOldValue, float flOldValue )
|
|
{
|
|
Steam3Server().NotifyOfServerNameChange();
|
|
}
|
|
|
|
ConVar tv_maxclients( "tv_maxclients", "128", FCVAR_RELEASE, "Maximum client number on GOTV server.",
|
|
true, 0, true, 255 );
|
|
ConVar tv_maxclients_relayreserved( "tv_maxclients_relayreserved", "0", FCVAR_RELEASE, "Reserves a certain number of GOTV client slots for relays.",
|
|
true, 0, true, 255 );
|
|
|
|
ConVar tv_autorecord( "tv_autorecord", "0", FCVAR_RELEASE, "Automatically records all games as GOTV demos." );
|
|
void OnTvBroadcast( IConVar *var, const char *pOldValue, float flOldValue );
|
|
ConVar tv_broadcast( "tv_broadcast", "0", FCVAR_RELEASE, "Automatically broadcasts all games as GOTV demos through Steam.", OnTvBroadcast );
|
|
ConVar tv_broadcast1( "tv_broadcast1", "0", FCVAR_RELEASE, "Automatically broadcasts all games as GOTV[1] demos through Steam.", OnTvBroadcast );
|
|
ConVar tv_name( "tv_name", "GOTV", FCVAR_RELEASE, "GOTV host name", tv_name_changed_f );
|
|
static ConVar tv_password( "tv_password", "", FCVAR_NOTIFY | FCVAR_PROTECTED | FCVAR_DONTRECORD | FCVAR_RELEASE, "GOTV password for all clients" );
|
|
static ConVar tv_advertise_watchable( "tv_advertise_watchable", "0", FCVAR_NOTIFY | FCVAR_PROTECTED | FCVAR_DONTRECORD | FCVAR_RELEASE, "GOTV advertises the match as watchable via game UI, clients watching via UI will not need to type password" );
|
|
|
|
static ConVar tv_overridemaster( "tv_overridemaster", "0", FCVAR_RELEASE, "Overrides the GOTV master root address." );
|
|
static ConVar tv_dispatchmode( "tv_dispatchmode", "1", FCVAR_RELEASE, "Dispatch clients to relay proxies: 0=never, 1=if appropriate, 2=always" );
|
|
static ConVar tv_dispatchweight( "tv_dispatchweight", "1.25", FCVAR_RELEASE, "Dispatch clients to relay proxies based on load, 1.25 will prefer for every 4 local clients to put 5 clients on every connected relay" );
|
|
ConVar tv_transmitall( "tv_transmitall", "1", FCVAR_REPLICATED | FCVAR_RELEASE, "Transmit all entities (not only director view)" );
|
|
ConVar tv_debug( "tv_debug", "0", FCVAR_RELEASE, "GOTV debug info." );
|
|
ConVar tv_title( "tv_title", "GOTV", FCVAR_RELEASE, "Set title for GOTV spectator UI", tv_title_changed_f );
|
|
static ConVar tv_deltacache( "tv_deltacache", "2", FCVAR_RELEASE, "Enable delta entity bit stream cache" );
|
|
static ConVar tv_relayvoice( "tv_relayvoice", "1", FCVAR_RELEASE, "Relay voice data: 0=off, 1=on" );
|
|
static ConVar tv_encryptdata_key( "tv_encryptdata_key", "", FCVAR_RELEASE, "When set to a valid key communication messages will be encrypted for GOTV" );
|
|
static ConVar tv_encryptdata_key_pub( "tv_encryptdata_key_pub", "", FCVAR_RELEASE, "When set to a valid key public communication messages will be encrypted for GOTV" );
|
|
|
|
static ConVar tv_window_size( "tv_window_size", "16.0", FCVAR_NONE, "Specifies the number of seconds worth of frames that the tv replay system should keep in memory. Increasing this greatly increases the amount of memory consumed by the TV system" );
|
|
static ConVar tv_enable_delta_frames( "tv_enable_delta_frames", "1", FCVAR_RELEASE, "Indicates whether or not the tv should use delta frames for storage of intermediate frames. This takes more CPU but significantly less memory." );
|
|
extern ConVar spec_replay_enable;
|
|
extern ConVar spec_replay_message_time;
|
|
ConVar spec_replay_leadup_time( "spec_replay_leadup_time", "5.3438", FCVAR_RELEASE | FCVAR_REPLICATED, "Replay time in seconds before the highlighted event" );
|
|
|
|
ConVar tv_broadcast_url( "tv_broadcast_url", "http://localhost:8080", FCVAR_RELEASE, "URL of the broadcast relay" );
|
|
|
|
|
|
CHLTVServer::SHLTVDeltaFrame_t::SHLTVDeltaFrame_t() :
|
|
m_pRelativeFrame( NULL ),
|
|
m_pSourceFrame( NULL ),
|
|
m_pClientFrame( NULL ),
|
|
m_nNumValidEntities( 0 ),
|
|
m_nTotalEntities( 0 ),
|
|
m_pEntities( NULL ),
|
|
m_pNewerDeltaFrame( NULL )
|
|
{}
|
|
|
|
CHLTVServer::SHLTVDeltaFrame_t::~SHLTVDeltaFrame_t()
|
|
{
|
|
delete [] m_pCopyEntities;
|
|
m_pCopyEntities = NULL;
|
|
|
|
//delete the client frame
|
|
delete m_pClientFrame;
|
|
m_pClientFrame = NULL;
|
|
|
|
//delete our entity linked list
|
|
while( m_pEntities )
|
|
{
|
|
SHLTVDeltaEntity_t* pDel = m_pEntities;
|
|
m_pEntities = m_pEntities->m_pNext;
|
|
delete pDel;
|
|
}
|
|
|
|
if( m_pRelativeFrame )
|
|
m_pRelativeFrame->ReleaseReference();
|
|
|
|
if( m_pSourceFrame )
|
|
m_pSourceFrame->ReleaseReference();
|
|
}
|
|
|
|
CHLTVServer::SHLTVDeltaEntity_t::SHLTVDeltaEntity_t() :
|
|
m_SerializedEntity( SERIALIZED_ENTITY_HANDLE_INVALID ),
|
|
m_pServerClass( NULL ),
|
|
m_pNewRecipients( NULL ),
|
|
m_pNext( NULL )
|
|
{
|
|
}
|
|
|
|
CHLTVServer::SHLTVDeltaEntity_t::~SHLTVDeltaEntity_t()
|
|
{
|
|
//free any recipients if we allocated them
|
|
delete [] m_pNewRecipients;
|
|
|
|
if( (m_SerializedEntity != knNoPackedData) && ( m_SerializedEntity != SERIALIZED_ENTITY_HANDLE_INVALID ) )
|
|
delete ( CSerializedEntity* )m_SerializedEntity;
|
|
}
|
|
|
|
|
|
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->pNext = pEntry = pNew;
|
|
}
|
|
|
|
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( const 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].GetData(), frame.m_Messages[HLTV_BUFFER_RELIABLE].GetData(), 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 ( 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() )
|
|
{
|
|
int nVoiceBits = frame.m_Messages[ HLTV_BUFFER_VOICE ].GetNumBitsWritten();
|
|
if ( nVoiceBits > 0 )
|
|
{
|
|
int nVoiceBytes = PAD_NUMBER( Bits2Bytes( nVoiceBits ), 4 );
|
|
m_Messages[ HLTV_BUFFER_VOICE ].StartWriting( new char[ nVoiceBytes ], nVoiceBytes );
|
|
m_Messages[ HLTV_BUFFER_VOICE ].WriteBits( frame.m_Messages[ HLTV_BUFFER_VOICE ].GetData(), frame.m_Messages[ HLTV_BUFFER_VOICE ].GetNumBitsWritten() );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
uint CHLTVFrame::GetMemSize()const
|
|
{
|
|
uint nSize = sizeof( *this );
|
|
for ( int i = 0; i < ARRAYSIZE( m_Messages ); ++i )
|
|
nSize += m_Messages[ i ].GetMaxNumBits() / 8;
|
|
return nSize;
|
|
}
|
|
|
|
|
|
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( uint nInstanceIndex, float flSnapshotRate )
|
|
: m_DemoRecorder( this )
|
|
, m_Broadcast( this )
|
|
, m_ClientState( this )
|
|
, m_nInstanceIndex( nInstanceIndex )
|
|
, m_flSnapshotRate( flSnapshotRate )
|
|
{
|
|
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;
|
|
Assert( m_ClientState.m_pHLTV == this ); // constructor of ClientState should've set thie HLTV.
|
|
m_nGlobalSlots = 0;
|
|
m_nGlobalClients = 0;
|
|
m_nGlobalProxies = 0;
|
|
m_nExternalTotalViewers = 0;
|
|
m_nExternalLinkedViewers = 0;
|
|
|
|
m_nDebugID = EVENT_DEBUG_ID_INIT;
|
|
|
|
m_pOldestDeltaFrame = NULL;
|
|
m_pNewestDeltaFrame = NULL;
|
|
|
|
m_pLastSourceSnapshot = NULL;
|
|
m_pLastTargetSnapshot = NULL;
|
|
}
|
|
|
|
CHLTVServer::~CHLTVServer()
|
|
{
|
|
m_nDebugID = EVENT_DEBUG_ID_SHUTDOWN;
|
|
|
|
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)
|
|
{
|
|
m_nExternalTotalViewers = 0;
|
|
m_nExternalLinkedViewers = 0;
|
|
Clear(); // clear old settings & buffers
|
|
|
|
if ( !client )
|
|
{
|
|
ConMsg("GOTV client not found.\n");
|
|
return;
|
|
}
|
|
|
|
m_Director = serverGameDirector;
|
|
|
|
if ( !m_Director )
|
|
{
|
|
ConMsg("Mod doesn't support GOTV. No director module found.\n");
|
|
return;
|
|
}
|
|
|
|
m_MasterClient = client;
|
|
m_MasterClient->m_bIsHLTV = true;
|
|
m_MasterClient->m_pHltvSlaveServer = this; // Master client needs to know which server (with which tickrate) it's sending packets
|
|
#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", va( "%d", DEFAULT_RATE ) );
|
|
m_MasterClient->m_ConVars->SetString( "cl_updaterate", va( "%f", GetSnapshotRate() ) ); // this may not be necessary...
|
|
m_MasterClient->m_ConVars->SetString( "cl_cmdrate", "64" );
|
|
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;
|
|
worldmapCRC = m_Server->worldmapCRC;
|
|
clientDllCRC = m_Server->clientDllCRC;
|
|
m_flTickInterval= m_Server->GetTickInterval();
|
|
|
|
// allocate buffers for input frame
|
|
m_HLTVFrame.AllocBuffers();
|
|
|
|
InstallStringTables();
|
|
|
|
// activate director in game.dll
|
|
m_Director->AddHLTVServer( this ); // we RemoveHLTVServer later instead of setting it to NULL now
|
|
|
|
// 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
|
|
StopRecording();
|
|
|
|
// start new recording if autorecord is enabled
|
|
if ( tv_autorecord.GetBool() )
|
|
{
|
|
m_DemoRecorder.StartAutoRecording();
|
|
}
|
|
m_Broadcast.OnMasterStarted();
|
|
if ( GetIndexedConVar( tv_broadcast, GetInstanceIndex() ).GetBool() )
|
|
{
|
|
StartBroadcast();
|
|
}
|
|
|
|
m_DemoEventWriteBuffer.StartWriting( m_DemoEventsBuffer, ARRAYSIZE( m_DemoEventsBuffer) );
|
|
|
|
ReconnectClients();
|
|
}
|
|
|
|
|
|
void CHLTVServer::StartBroadcast()
|
|
{
|
|
m_Broadcast.StartRecording( tv_broadcast_url.GetString() );
|
|
}
|
|
|
|
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 GOTV relay available");
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// just let client connect to this proxy
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// check if client should stay on this relay server unless we are the master,
|
|
// masters always prefer to send clients to relays
|
|
if ( (tv_dispatchmode.GetInt() == DISPATCH_MODE_AUTO) && (GetMaxClients() > 0) )
|
|
{
|
|
// ratio = clients/slots. give relay proxies 25% bonus
|
|
int numSlots = GetMaxClients();
|
|
if ( tv_maxclients_relayreserved.GetInt() > 0 )
|
|
numSlots -= tv_maxclients_relayreserved.GetInt();
|
|
numSlots = MAX( 0, numSlots );
|
|
|
|
int numClients = GetNumClients();
|
|
if ( numClients > numSlots )
|
|
numSlots = numClients;
|
|
|
|
float flDispatchWeight = tv_dispatchweight.GetFloat();
|
|
if ( flDispatchWeight <= 1.01 )
|
|
flDispatchWeight = 1.01;
|
|
float myRatio = ((float)numClients/(float)numSlots) * flDispatchWeight;
|
|
|
|
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 GOTV relay %s\n",
|
|
pClient->GetNetChannel()->GetAddress(),
|
|
pszRelayAddr );
|
|
|
|
// first tell that client that we are a SourceTV server,
|
|
// otherwise it's might ignore the "connect" command
|
|
CSVCMsg_ServerInfo_t serverInfo;
|
|
FillServerInfo( serverInfo );
|
|
serverInfo.set_is_redirecting_to_proxy_relay( true );
|
|
pClient->SendNetMsg( serverInfo, true );
|
|
|
|
// tell the client to connect to this new address
|
|
CNETMsg_StringCmd_t cmdMsg( va("connect %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)
|
|
{
|
|
m_nExternalTotalViewers = 0;
|
|
m_nExternalLinkedViewers = 0;
|
|
|
|
if ( m_ClientState.IsConnected() )
|
|
{
|
|
// do not try to reconnect to old connection
|
|
m_ClientState.m_Remote.RemoveAll();
|
|
|
|
// disconnect first
|
|
m_ClientState.Disconnect();
|
|
|
|
Changelevel( true ); // inactivate clients
|
|
}
|
|
|
|
// connect to new server
|
|
m_ClientState.Connect( address, address, "HLTVConnectRelay" );
|
|
}
|
|
|
|
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 )
|
|
{
|
|
int numSlots = GetMaxClients();
|
|
if ( tv_maxclients_relayreserved.GetInt() > 0 )
|
|
numSlots -= tv_maxclients_relayreserved.GetInt();
|
|
numSlots = MAX( 0, numSlots );
|
|
|
|
int numClients = GetNumClients();
|
|
if ( numClients > numSlots )
|
|
numSlots = numClients;
|
|
|
|
proxies = GetNumProxies();
|
|
clients = numClients;
|
|
slots = numSlots;
|
|
}
|
|
|
|
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 );
|
|
|
|
int numSlots = GetMaxClients();
|
|
if ( tv_maxclients_relayreserved.GetInt() > 0 )
|
|
numSlots -= tv_maxclients_relayreserved.GetInt();
|
|
numSlots = MAX( 0, numSlots );
|
|
|
|
int numClients = GetNumClients();
|
|
if ( !numClients && m_Broadcast.IsRecording() )
|
|
{ // Always consider broadcast stream as a non-zero amount of clients
|
|
numClients = 1;
|
|
}
|
|
|
|
if ( numClients > numSlots )
|
|
numSlots = numClients;
|
|
|
|
m_nGlobalSlots += numSlots;
|
|
m_nGlobalClients += numClients;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
void CHLTVServer::GetExternalStats( int &numExternalTotalViewers, int &numExternalLinkedViewers )
|
|
{
|
|
numExternalTotalViewers = m_nExternalTotalViewers;
|
|
numExternalLinkedViewers = m_nExternalLinkedViewers;
|
|
}
|
|
|
|
void CHLTVServer::UpdateHltvExternalViewers( uint32 numTotalViewers, uint32 numLinkedViewers )
|
|
{
|
|
if ( !IsMasterProxy() )
|
|
return;
|
|
if ( !IsActive() )
|
|
return;
|
|
|
|
m_nExternalTotalViewers = numTotalViewers;
|
|
m_nExternalLinkedViewers = numLinkedViewers;
|
|
}
|
|
|
|
const netadr_t *CHLTVServer::GetRelayAddress( void )
|
|
{
|
|
if ( IsMasterProxy() )
|
|
{
|
|
return &net_local_adr; // TODO wrong port
|
|
}
|
|
else if ( m_ClientState.m_NetChannel )
|
|
{
|
|
const ns_address &adr = m_ClientState.m_NetChannel->GetRemoteAddress();
|
|
if ( adr.IsType<netadr_t>() )
|
|
return &adr.AsType<netadr_t>();
|
|
}
|
|
|
|
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() );
|
|
|
|
CSVCMsg_GameEvent_t eventMsg;
|
|
|
|
eventMsg.SetReliable( true );
|
|
|
|
// create bit stream from KeyValues
|
|
if ( !g_GameEventManager.SerializeEvent( event, &eventMsg ) )
|
|
{
|
|
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 );
|
|
|
|
CSVCMsg_GameEvent_t eventMsg;
|
|
|
|
eventMsg.SetReliable( false );
|
|
|
|
// create bit stream from KeyValues
|
|
if ( !g_GameEventManager.SerializeEvent( event, &eventMsg ) )
|
|
{
|
|
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 )
|
|
{
|
|
CSVCMsg_GameEvent_t eventMsg;
|
|
|
|
eventMsg.SetReliable( bReliable );
|
|
|
|
// create bit stream from KeyValues
|
|
if ( !g_GameEventManager.SerializeEvent( event, &eventMsg ) )
|
|
{
|
|
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)
|
|
{
|
|
CSVCMsg_GameEvent_t eventMsg;
|
|
|
|
// create bit stream from KeyValues
|
|
if ( !g_GameEventManager.SerializeEvent( event, &eventMsg ) )
|
|
{
|
|
DevMsg("CHLTVServer: failed to serialize event '%s'.\n", event->GetName() );
|
|
return;
|
|
}
|
|
|
|
BroadcastMessage( eventMsg, true, true );
|
|
|
|
if ( m_DemoRecorder.IsRecording() || m_Broadcast.IsRecording() )
|
|
{
|
|
eventMsg.WriteToBuffer( m_DemoEventWriteBuffer );
|
|
}
|
|
|
|
if ( tv_debug.GetBool() )
|
|
Msg("SourceTV broadcast event: %s\n", event->GetName() );
|
|
}
|
|
|
|
bool CHLTVServer::IsRecording()
|
|
{
|
|
return m_DemoRecorder.IsRecording();
|
|
}
|
|
|
|
const char* CHLTVServer::GetRecordingDemoFilename()
|
|
{
|
|
return m_DemoRecorder.GetDemoFilename();
|
|
}
|
|
|
|
void CHLTVServer::StartAutoRecording()
|
|
{
|
|
return m_DemoRecorder.StartAutoRecording();
|
|
}
|
|
|
|
void CHLTVServer::FireGameEvent(IGameEvent *event)
|
|
{
|
|
if ( !IsActive() )
|
|
return;
|
|
|
|
CSVCMsg_GameEvent_t eventMsg;
|
|
|
|
// create bit stream from KeyValues
|
|
if ( g_GameEventManager.SerializeEvent( event, &eventMsg ) )
|
|
{
|
|
SendNetMsg( eventMsg );
|
|
}
|
|
else
|
|
{
|
|
DevMsg("CHLTVServer::FireGameEvent: failed to serialize event '%s'.\n", event->GetName() );
|
|
}
|
|
}
|
|
|
|
int CHLTVServer::GetEventDebugID( void )
|
|
{
|
|
return m_nDebugID;
|
|
}
|
|
|
|
//Whenever we're done recording a demo, go through all the remaining frames and write them too.
|
|
//This is done because now the demo recorded is recuring the CURRENT frame instead of the broadcast frame
|
|
void CHLTVServer::StopRecordingAndFreeFrames( bool bFreeFrames, const CGameInfo *pGameInfo)
|
|
{
|
|
// We are only stopping demo recorder. Broadcast does not stop on changelevel, for example. If broadcast is activated, it stays activated until stopped.
|
|
if( !m_CurrentFrame || !m_DemoRecorder.IsRecording() )
|
|
{
|
|
//we aren't recording, just clean up if requested
|
|
if( bFreeFrames )
|
|
{
|
|
DeleteClientFrames( -1 );
|
|
FreeAllDeltaFrames();
|
|
m_CurrentFrame = NULL;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
int nStartingTick = m_CurrentFrame->tick_count;
|
|
CHLTVFrame *pFrame = static_cast< CHLTVFrame *> (m_CurrentFrame->m_pNext );
|
|
|
|
//clear our current frame since we are flushing all of our frames
|
|
if( bFreeFrames )
|
|
m_CurrentFrame = NULL;
|
|
|
|
//flush any remaining current frames that we have
|
|
while ( pFrame )
|
|
{
|
|
// restore string tables for this time
|
|
int nFrameTick = pFrame->tick_count;
|
|
RestoreTick( pFrame->tick_count );
|
|
m_DemoRecorder.WriteFrame( pFrame );
|
|
pFrame = static_cast< CHLTVFrame *> (pFrame->m_pNext );
|
|
|
|
//and clean up the memory for this frame if we need to
|
|
if( bFreeFrames )
|
|
DeleteClientFrames( nFrameTick );
|
|
}
|
|
|
|
//now we need to handle the delta frames, freeing as we go to avoid a memory explosion
|
|
while( m_pOldestDeltaFrame )
|
|
{
|
|
CHLTVFrame* pExpandedFrame = m_pOldestDeltaFrame->m_pClientFrame;
|
|
int nExpandFrameTick = pExpandedFrame->tick_count;
|
|
|
|
ExpandDeltaFramesToTick( nExpandFrameTick );
|
|
RestoreTick( nExpandFrameTick );
|
|
m_DemoRecorder.WriteFrame( pExpandedFrame );
|
|
|
|
//and clean up the memory for this frame if we need to
|
|
if( bFreeFrames )
|
|
DeleteClientFrames( nExpandFrameTick );
|
|
}
|
|
|
|
// restore our string tables to what they were to begin with
|
|
RestoreTick( nStartingTick );
|
|
|
|
return m_DemoRecorder.StopRecording( pGameInfo );
|
|
}
|
|
|
|
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->CreateStringTable(
|
|
serverTable->GetTableName(),
|
|
serverTable->GetMaxStrings(),
|
|
serverTable->GetUserDataSize(),
|
|
serverTable->GetUserDataSizeBits(),
|
|
serverTable->IsUsingDictionary() ? NSF_DICTIONARY_ENABLED : NSF_NONE
|
|
);
|
|
|
|
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( m_nInstanceIndex, hltvTable ); // there may be multiple mirror tables (up to the number of HLTV servers), it's easier to manage than removing the mirror tables from the correct places at the correct times
|
|
}
|
|
|
|
m_StringTables->AllowCreation( false );
|
|
|
|
#endif
|
|
}
|
|
|
|
void CHLTVServer::UninstallStringTables( void )
|
|
{
|
|
if ( !m_Server )
|
|
return;
|
|
int numTables = m_Server->m_StringTables->GetNumTables();
|
|
|
|
for ( int i = 0; i < numTables; i++ )
|
|
{
|
|
// iterate through server tables
|
|
CNetworkStringTable *serverTable =
|
|
( CNetworkStringTable* )m_Server->m_StringTables->GetTable( i );
|
|
|
|
if ( !serverTable )
|
|
continue;
|
|
|
|
serverTable->SetMirrorTable( m_nInstanceIndex, NULL ); // alternatively, we could find serverTable again and remove mirror table by pointer. It would be a bit more fragile and slower.
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
bool CHLTVServer::GetClassBaseline( ServerClass *pClass, SerializedEntityHandle_t *pHandle)
|
|
{
|
|
// if we are the master proxy the game server has our baseline data
|
|
if ( m_Server )
|
|
{
|
|
return m_Server->GetClassBaseline( pClass, pHandle );
|
|
}
|
|
|
|
// otherwise we have it from the hltvclientstate
|
|
return m_ClientState.GetClassBaseline( pClass->m_ClassID, pHandle );
|
|
}
|
|
|
|
void CHLTVServer::LinkInstanceBaselines( void )
|
|
{
|
|
// Forces to update m_pInstanceBaselineTable.
|
|
AUTO_LOCK_FM( 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("GOTV signon complete.\n" );
|
|
}
|
|
|
|
CFrameSnapshot* CHLTVServer::CloneDeltaSnapshot( const CFrameSnapshot *pCopySnapshot )
|
|
{
|
|
//and create a new snapshot for this to reference, of which will not have entity data
|
|
CFrameSnapshot* pNewSnapshot = framesnapshotmanager->CreateEmptySnapshot(
|
|
#ifdef DEBUG_SNAPSHOT_REFERENCES
|
|
"CHLTVServer::CloneDeltaSnapshot",
|
|
#endif
|
|
pCopySnapshot->m_nTickCount, 0, knHLTVSnapshotSet );
|
|
|
|
//copy over snapshot data...
|
|
pNewSnapshot->m_iExplicitDeleteSlots = pCopySnapshot->m_iExplicitDeleteSlots;
|
|
pNewSnapshot->m_nTickCount = pCopySnapshot->m_nTickCount;
|
|
|
|
//note that we do not copy over the valid entities list. We only copy over info for valid entities, so it is already properly collapsed, and we can save memory by ignoring this
|
|
pNewSnapshot->m_pValidEntities = NULL;
|
|
pNewSnapshot->m_nValidEntities = 0;
|
|
|
|
//we don't need to copy over the events. The HLTV frame will actually copy over all the bits from the frame for the temporary entities, so we do not need to serialize this
|
|
pNewSnapshot->m_nTempEntities = 0;
|
|
pNewSnapshot->m_pTempEntities = NULL;
|
|
|
|
return pNewSnapshot;
|
|
}
|
|
|
|
//the bit buffer assumes that all buffers it reads/writes are 4 byte aligned, which the serialized entity enforces, so this function given a number of bits, will ensure that the proper size
|
|
//in bytes for the buffer is met
|
|
static int GetBitBufBytes( int nNumBits )
|
|
{
|
|
return ( ( nNumBits + 31 ) / 32) * 4;
|
|
}
|
|
|
|
//given a tick that the time changed on, and the current value along with the previous properties, this will determine which properties
|
|
//have changed, and create a serialized entity handle that contains just the delta properties
|
|
static SerializedEntityHandle_t CreateDeltaProperties( int nTick, const PackedEntity* pCurrPacked, const PackedEntity *pDeltaBase )
|
|
{
|
|
//the actual max properties is HUGE, and we only need deltas, so to avoid blowing up the stack, we use a smaller buffer size
|
|
static const int knMaxChangedProps = 4 * 1024;
|
|
uint16 nChangedPropIndices[ knMaxChangedProps ];
|
|
|
|
//get our source serialized entity data
|
|
const CSerializedEntity* pCurrProps = ( const CSerializedEntity* )pCurrPacked->GetPackedData();
|
|
|
|
//if we have no delta base to encode from, we just want the entire structure
|
|
if( !pDeltaBase )
|
|
{
|
|
CSerializedEntity *pNewSerialized = new CSerializedEntity( );
|
|
pNewSerialized->Copy( *pCurrProps );
|
|
return ( SerializedEntityHandle_t )pNewSerialized;
|
|
}
|
|
|
|
//we have a delta, so lets build only the properties that vary based upon the tick time
|
|
const int nNumProps = pCurrProps->GetFieldCount();
|
|
|
|
//first pass, determine all the memory that we'll need for everything
|
|
const CChangeFrameList *pChangeList = pCurrPacked->GetChangeFrameList();
|
|
|
|
int nNumChangedProps = 0;
|
|
int nChangedPropDataBits = 0;
|
|
|
|
for( int nCurrProp = 0; nCurrProp < nNumProps; ++nCurrProp )
|
|
{
|
|
CFieldPath PropIndex = pCurrProps->GetFieldPath( nCurrProp );
|
|
|
|
if( pChangeList->DidPropChangeAfterTick( nTick, PropIndex ) )
|
|
{
|
|
//this property changed, so add it into our list
|
|
AssertMsg( nNumChangedProps < knMaxChangedProps, "Error: Overflow in modified properties for delta compression. This limit should be increased" );
|
|
nChangedPropIndices[ nNumChangedProps ] = nCurrProp;
|
|
nNumChangedProps++;
|
|
nChangedPropDataBits += pCurrProps->GetFieldDataSizeInBits( nCurrProp );
|
|
}
|
|
}
|
|
|
|
//if we have no changed properties, excellent, we can just bail and not have to spend memory
|
|
if( nNumChangedProps == 0 )
|
|
return SERIALIZED_ENTITY_HANDLE_INVALID;
|
|
|
|
//we have changed props, so go ahead and allocate a buffer to hold the memory that we are going to setup
|
|
CSerializedEntity *pNewSerialized = new CSerializedEntity( );
|
|
pNewSerialized->SetupPackMemory( nNumChangedProps, nChangedPropDataBits );
|
|
|
|
//setup the readers and writers
|
|
bf_read SrcPropData;
|
|
pCurrProps->StartReading( SrcPropData );
|
|
bf_write OutPropData;
|
|
pNewSerialized->StartWriting( OutPropData );
|
|
|
|
//and now run through and copy over the data that we need
|
|
for( int nOutputProp = 0; nOutputProp < nNumChangedProps; ++nOutputProp )
|
|
{
|
|
int nDataOffset, nNextOffset;
|
|
CFieldPath PropIndex;
|
|
pCurrProps->GetField( nChangedPropIndices[ nOutputProp ], PropIndex, &nDataOffset, &nNextOffset );
|
|
|
|
//record this information in our buffer
|
|
pNewSerialized->SetFieldPath( nOutputProp, PropIndex );
|
|
pNewSerialized->SetFieldDataBitOffset( nOutputProp, OutPropData.GetNumBitsWritten() );
|
|
|
|
//copy over the contents that we care about (which are bit aligned)
|
|
SrcPropData.Seek( nDataOffset );
|
|
OutPropData.WriteBitsFromBuffer( &SrcPropData, nNextOffset - nDataOffset );
|
|
}
|
|
|
|
//make sure our size calculations line up
|
|
Assert( ( uint32 )OutPropData.GetNumBitsWritten() == pNewSerialized->GetFieldDataBitCount() );
|
|
|
|
//we have our final property set now
|
|
return ( SerializedEntityHandle_t )pNewSerialized;
|
|
}
|
|
|
|
//given a current and previous packed data, this will determine if the entity data changed, and if so, will setup the new recipient list with the appropraite information
|
|
static CSendProxyRecipients* CopyRecipientList( const PackedEntity* pCurrFramePacked, const PackedEntity* pPrevFramePacked )
|
|
{
|
|
if( ( pPrevFramePacked != NULL ) && pCurrFramePacked->CompareRecipients( CUtlMemory< CSendProxyRecipients >( pPrevFramePacked->GetRecipients(), pPrevFramePacked->GetNumRecipients() ) ) )
|
|
{
|
|
//they match, so we don't need a delta buffer
|
|
return NULL;
|
|
}
|
|
|
|
//if we got down here there is a mismatch, so just copy the new list
|
|
const int nCurrRecipients = pCurrFramePacked->GetNumRecipients();
|
|
CSendProxyRecipients* pCopyDest = new CSendProxyRecipients [ nCurrRecipients ];
|
|
const CSendProxyRecipients* pCopySrc = pCurrFramePacked->GetRecipients();
|
|
|
|
for( int nCopyRecipient = 0; nCopyRecipient < nCurrRecipients; ++nCopyRecipient )
|
|
pCopyDest[ nCopyRecipient ] = pCopySrc[ nCopyRecipient ];
|
|
|
|
return pCopyDest;
|
|
}
|
|
|
|
void CHLTVServer::CreateDeltaFrameEntities( SHLTVDeltaFrame_t *pOutputEntities, const CFrameSnapshot *pCurrFrame, const CFrameSnapshot *pPrevFrame )
|
|
{
|
|
//determine how many ints we need to store our bit list
|
|
const uint32 nNumCopyInts = ( pCurrFrame->m_nNumEntities + 31 ) / 32;
|
|
|
|
//allocate memory for us to actually store the objects
|
|
pOutputEntities->m_nTotalEntities = pCurrFrame->m_nNumEntities;
|
|
pOutputEntities->m_nNumValidEntities = pCurrFrame->m_nValidEntities;
|
|
pOutputEntities->m_pEntities = NULL;
|
|
pOutputEntities->m_pCopyEntities = new uint32 [ nNumCopyInts ];
|
|
memset( pOutputEntities->m_pCopyEntities, 0, sizeof(uint32) * nNumCopyInts );
|
|
|
|
//the index into our current previous frame so that we can find old versions of the object to delta against
|
|
const uint16 *pPrevEntityIndex = ( pPrevFrame ) ? pPrevFrame->m_pValidEntities : NULL;
|
|
const uint16 *pPrevEndEntityIndex = ( pPrevFrame ) ? pPrevEntityIndex + pPrevFrame->m_nValidEntities : NULL;
|
|
|
|
//the tick that we want to compare changes against
|
|
int nChangeTick = ( pPrevFrame ) ? pPrevFrame->m_nTickCount : -1;
|
|
|
|
//the current tail of our linked list
|
|
SHLTVDeltaEntity_t** pListTail = &pOutputEntities->m_pEntities;
|
|
|
|
//don't iterate through the entities, but instead iterate through our valid entity list, which indexes into our entities so we can
|
|
//skip invalid entities
|
|
for( int nValidEntity = 0; nValidEntity < pCurrFrame->m_nValidEntities; ++nValidEntity )
|
|
{
|
|
const int nEntityIndex = pCurrFrame->m_pValidEntities[ nValidEntity ];
|
|
const CFrameSnapshotEntry *pSrcEntity = pCurrFrame->m_pEntities + nEntityIndex;
|
|
|
|
//see if we can find a previous entity in order to diff ourself against
|
|
bool bReuseOriginal = false;
|
|
const PackedEntity* pPrevFramePacked = NULL;
|
|
for( ; pPrevEntityIndex != pPrevEndEntityIndex; ++pPrevEntityIndex )
|
|
{
|
|
//see if our entity is higher than our current one, if so, we need to stop searching and let our current frame list catch up
|
|
if( *pPrevEntityIndex > nEntityIndex )
|
|
break;
|
|
|
|
//if the slot matches, then we may have a match
|
|
if( *pPrevEntityIndex == nEntityIndex )
|
|
{
|
|
const CFrameSnapshotEntry *pPrevEntity = &pPrevFrame->m_pEntities[ *pPrevEntityIndex ];
|
|
|
|
//we found a match. See if it is actually the same object
|
|
if( pPrevEntity->m_nSerialNumber == pSrcEntity->m_nSerialNumber )
|
|
{
|
|
//same object, so we can reuse the data, but first see if it is identical
|
|
if( pPrevEntity->m_pPackedData == pSrcEntity->m_pPackedData )
|
|
{
|
|
//it is identical, we can just reuse it
|
|
bReuseOriginal = true;
|
|
}
|
|
else
|
|
{
|
|
//different, so cache the data
|
|
pPrevFramePacked = framesnapshotmanager->GetPackedEntity( *( const_cast< CFrameSnapshot* >(pPrevFrame) ), nEntityIndex );
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
//first off, see if this is an entity that hasn't changed at all
|
|
if( bReuseOriginal )
|
|
{
|
|
//this is the same entity, so just copy it forward
|
|
pOutputEntities->m_pCopyEntities[ nEntityIndex / 32 ] |= ( 1 << ( nEntityIndex % 32 ) );
|
|
//skip the normal handling
|
|
continue;
|
|
}
|
|
|
|
//allocate our new entity and add it to our list
|
|
SHLTVDeltaEntity_t *pOutEntity = new SHLTVDeltaEntity_t;
|
|
*pListTail = pOutEntity;
|
|
pListTail = &pOutEntity->m_pNext;
|
|
|
|
//copy over all the state we can from this entity
|
|
pOutEntity->m_nSerialNumber = pSrcEntity->m_nSerialNumber;
|
|
pOutEntity->m_pServerClass = pSrcEntity->m_pClass;
|
|
pOutEntity->m_nSourceIndex = nEntityIndex;
|
|
|
|
//handle the situation where there is no packed data (occurs occasionally)
|
|
if( pSrcEntity->m_pPackedData == INVALID_PACKED_ENTITY_HANDLE )
|
|
{
|
|
pOutEntity->m_SerializedEntity = SHLTVDeltaEntity_t::knNoPackedData;
|
|
}
|
|
else
|
|
{
|
|
//along with the data from the packed portion of the entity
|
|
const PackedEntity *pSrcPacked = framesnapshotmanager->GetPackedEntity( *( const_cast< CFrameSnapshot* >(pCurrFrame) ), nEntityIndex );
|
|
|
|
//this is a new packed entity that we'll need to construct on the other side
|
|
pOutEntity->m_nSnapshotCreationTick = pSrcPacked->GetSnapshotCreationTick( );
|
|
|
|
//build up the property list for this based upon how we want to encode it (absolute, relative, etc)
|
|
pOutEntity->m_SerializedEntity = CreateDeltaProperties( nChangeTick, pSrcPacked, pPrevFramePacked );
|
|
|
|
//update our recipient list
|
|
pOutEntity->m_nNumRecipients = ( uint16 )pSrcPacked->GetNumRecipients();
|
|
pOutEntity->m_pNewRecipients = CopyRecipientList( pSrcPacked, pPrevFramePacked );
|
|
}
|
|
}
|
|
}
|
|
|
|
void CHLTVServer::AddNewDeltaFrame( CClientFrame *pClientFrame )
|
|
{
|
|
//see if this support is disabled
|
|
if( !tv_enable_delta_frames.GetBool()
|
|
#if HLTV_REPLAY_ENABLED
|
|
|| spec_replay_enable.GetBool() // <sergiy> delta frames with small delay make no sense because we'll effectively only compress a second or two ( the min delay )of the packets. Enabling replay means the first 20 seconds of frames must be decompressed. Maybe we'll implement compression between 20 and 100 seconds for competitive sometimes, but for now it seems ok to leave frames uncompressed if we want replay
|
|
#endif
|
|
)
|
|
{
|
|
//we aren't running delta frames, so make sure we decompress any we might currently have, and fall back to normal
|
|
ExpandDeltaFramesToTick( -1 );
|
|
AddNewFrame( pClientFrame );
|
|
return;
|
|
}
|
|
|
|
//track the performance of this frame
|
|
VPROF_BUDGET( "CHLTVServer::AddNewDeltaFrame", "HLTV" );
|
|
Assert( pClientFrame->tick_count > m_nLastTick );
|
|
|
|
//if we don't have any frames encoded, we will be doing an absolute encode, so we can just store the frame direct (saves a lot of time and allocations)
|
|
if( m_pLastSourceSnapshot == NULL )
|
|
{
|
|
Assert( ( m_pLastTargetSnapshot == NULL ) && ( m_nFirstTick < 0 ) );
|
|
|
|
//add the frame. This way it will be ready immediately, and we don't have to duplicate all of the setup work
|
|
AddNewFrame( pClientFrame );
|
|
|
|
//use this frame as our encoding relative frame, and our delta that we will build on later as well
|
|
m_pLastSourceSnapshot = pClientFrame->GetSnapshot();
|
|
m_pLastSourceSnapshot->AddReference();
|
|
m_pLastTargetSnapshot = pClientFrame->GetSnapshot();
|
|
m_pLastTargetSnapshot->AddReference();
|
|
|
|
return;
|
|
}
|
|
|
|
//update internal state based upon the incoming frames (this should match AddNewFrame)
|
|
m_nLastTick = pClientFrame->tick_count;
|
|
m_HLTVFrame.SetSnapshot( pClientFrame->GetSnapshot() );
|
|
m_HLTVFrame.tick_count = pClientFrame->tick_count;
|
|
m_HLTVFrame.last_entity = pClientFrame->last_entity;
|
|
m_HLTVFrame.transmit_entity = pClientFrame->transmit_entity;
|
|
|
|
//first off allocate our holding frame and client information
|
|
SHLTVDeltaFrame_t *pNewDeltaFrame = new SHLTVDeltaFrame_t;
|
|
pNewDeltaFrame->m_pNewerDeltaFrame = NULL;
|
|
pNewDeltaFrame->m_pRelativeFrame = NULL;
|
|
|
|
//Uncomment these lines if you want to enable validation support
|
|
{
|
|
//pNewDeltaFrame->m_pSourceFrame = pClientFrame->GetSnapshot();
|
|
//pNewDeltaFrame->m_pSourceFrame->AddReference();
|
|
}
|
|
|
|
//if we have a previously encoded frame, we want to use that as our relative baseline
|
|
if( m_pLastTargetSnapshot )
|
|
{
|
|
pNewDeltaFrame->m_pRelativeFrame = m_pLastTargetSnapshot;
|
|
pNewDeltaFrame->m_pRelativeFrame->AddReference();
|
|
}
|
|
|
|
//create our new frame and copy all the data over
|
|
pNewDeltaFrame->m_pClientFrame = new CHLTVFrame( );
|
|
pNewDeltaFrame->m_pClientFrame->CopyFrame( *pClientFrame );
|
|
pNewDeltaFrame->m_pClientFrame->CopyHLTVData( m_HLTVFrame );
|
|
|
|
//create a copy of the snapshot that we can work with
|
|
CFrameSnapshot *pNewSnapshot = CloneDeltaSnapshot( pClientFrame->GetSnapshot() );
|
|
|
|
//now we need to create our delta encoded objects
|
|
CreateDeltaFrameEntities( pNewDeltaFrame, pClientFrame->GetSnapshot(), m_pLastSourceSnapshot );
|
|
|
|
//transfer ownership of this snapshot over to the client frame (meaning we don't need our reference)
|
|
pNewDeltaFrame->m_pClientFrame->SetSnapshot(pNewSnapshot);
|
|
pNewSnapshot->ReleaseReference();
|
|
|
|
//link ourself into the list and make sure the newest link matches
|
|
if( m_pNewestDeltaFrame )
|
|
m_pNewestDeltaFrame->m_pNewerDeltaFrame = pNewDeltaFrame;
|
|
m_pNewestDeltaFrame = pNewDeltaFrame;
|
|
//and if our list was empty, our new frame is now also the oldest
|
|
if( !m_pOldestDeltaFrame )
|
|
m_pOldestDeltaFrame = pNewDeltaFrame;
|
|
|
|
// reset HLTV frame for recording next messages etc.
|
|
m_HLTVFrame.Reset();
|
|
m_HLTVFrame.SetSnapshot( NULL );
|
|
|
|
//update our references to point to our latest snapshots for subsequent encodes/decodes
|
|
if( m_pLastSourceSnapshot )
|
|
m_pLastSourceSnapshot->ReleaseReference();
|
|
if( m_pLastTargetSnapshot )
|
|
m_pLastTargetSnapshot->ReleaseReference();
|
|
|
|
m_pLastSourceSnapshot = pClientFrame->GetSnapshot();
|
|
m_pLastSourceSnapshot->AddReference();
|
|
m_pLastTargetSnapshot = pNewSnapshot;
|
|
m_pLastTargetSnapshot->AddReference();
|
|
}
|
|
|
|
//given a baseline property list and a delta from that, this will expand the delta serialized entity to contain a full property set
|
|
static void BuildMergedPropertySet( CSerializedEntity* pDelta, const CSerializedEntity* pBase, CChangeFrameList* pChangeTimes, int nChangeTick )
|
|
{
|
|
int nDataBlockSizeBits = pBase->GetFieldDataBitCount();
|
|
const int nNumFields = pBase->GetFieldCount();
|
|
|
|
//see if we can take an optimized path where we can memcpy over our baseline and just overlay
|
|
bool bCanMemCpySrc = true;
|
|
|
|
{
|
|
int nOverrideIndex = 0;
|
|
CFieldPath NextOverridePath = pDelta->GetFieldPath( 0 );
|
|
|
|
for( int nCurrField = 0; nCurrField < nNumFields; ++nCurrField )
|
|
{
|
|
//see if this is overridden
|
|
CFieldPath FieldPath= pBase->GetFieldPath( nCurrField );
|
|
if( FieldPath == NextOverridePath )
|
|
{
|
|
//we have a property change that we are applying, so update the change tick time
|
|
pChangeTimes->SetChangeTick( FieldPath, nChangeTick );
|
|
|
|
//use our modified size, minus our base size to determine the change in size from this override
|
|
int nBaseSize = pBase->GetFieldDataSizeInBits( nCurrField );
|
|
int nModifiedSize = pDelta->GetFieldDataSizeInBits( nOverrideIndex );
|
|
|
|
//see if this has changed the layout to the point where we have to re lay out the base fields
|
|
if( nModifiedSize != nBaseSize )
|
|
{
|
|
bCanMemCpySrc = false;
|
|
nDataBlockSizeBits += ( nModifiedSize - nBaseSize );
|
|
}
|
|
|
|
//and advance to our next override
|
|
nOverrideIndex++;
|
|
if( nOverrideIndex < pDelta->GetFieldCount( ) )
|
|
NextOverridePath = pDelta->GetFieldPath( nOverrideIndex );
|
|
}
|
|
}
|
|
}
|
|
|
|
//create a stack entity to take the new merged results
|
|
CSerializedEntity Merged;
|
|
Merged.SetupPackMemory( nNumFields, nDataBlockSizeBits );
|
|
|
|
if(bCanMemCpySrc)
|
|
{
|
|
memcpy( Merged.GetFieldData(), pBase->GetFieldData(), Bits2Bytes( pBase->GetFieldDataBitCount() ) );
|
|
}
|
|
|
|
//now merge
|
|
{
|
|
//setup the readers and writers
|
|
bf_read BaseData, ModifiedData;
|
|
pBase->StartReading( BaseData);
|
|
pDelta->StartReading( ModifiedData );
|
|
bf_write OutPropData;
|
|
Merged.StartWriting( OutPropData );
|
|
|
|
int nOverrideIndex = 0;
|
|
CFieldPath NextOverridePath = pDelta->GetFieldPath( 0 );
|
|
|
|
for( int nCurrField = 0; nCurrField < nNumFields; ++nCurrField )
|
|
{
|
|
//see if this is overridden
|
|
CFieldPath FieldPath = pBase->GetFieldPath( nCurrField );
|
|
|
|
//update our storage info
|
|
Merged.SetFieldPath( nCurrField, FieldPath );
|
|
Merged.SetFieldDataBitOffset( nCurrField, OutPropData.GetNumBitsWritten() );
|
|
|
|
if( FieldPath == NextOverridePath )
|
|
{
|
|
int nModifiedSize = pDelta->GetFieldDataSizeInBits( nOverrideIndex );
|
|
OutPropData.WriteBitsFromBuffer( &ModifiedData, nModifiedSize );
|
|
|
|
//and advance to our next override
|
|
nOverrideIndex++;
|
|
if( nOverrideIndex < pDelta->GetFieldCount( ) )
|
|
NextOverridePath = pDelta->GetFieldPath( nOverrideIndex );
|
|
}
|
|
else
|
|
{
|
|
//use the baseline value
|
|
int nDataStart = pBase->GetFieldDataBitOffset( nCurrField );
|
|
int nDataEnd = pBase->GetFieldDataBitEndOffset( nCurrField );
|
|
|
|
if( !bCanMemCpySrc )
|
|
{
|
|
BaseData.Seek( nDataStart );
|
|
OutPropData.WriteBitsFromBuffer( &BaseData, nDataEnd - nDataStart );
|
|
}
|
|
else
|
|
{
|
|
OutPropData.SeekToBit( nDataEnd );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//now we can discard our results of the delta, and just use that object with the contents of our merged
|
|
Merged.Swap( *pDelta );
|
|
}
|
|
|
|
static void CompareArray( int nOldElements, const void* pOldMem, int nNewElements, const void* pNewMem, int nElementSize )
|
|
{
|
|
//to avoid warnings when asserts aren't disabled
|
|
#ifdef DBGFLAG_ASSERT
|
|
|
|
Assert( nOldElements == nNewElements );
|
|
|
|
int nNumBytes = nOldElements * nElementSize;
|
|
const uint8* pByteOld = ( const uint8* )pOldMem;
|
|
const uint8* pByteNew = ( const uint8* )pNewMem;
|
|
|
|
//per byte check so it is more apparent where things went wrong
|
|
for( int nCurrByte = 0; nCurrByte < nNumBytes; ++nCurrByte )
|
|
{
|
|
Assert( pByteOld[ nCurrByte ] == pByteNew[ nCurrByte ] );
|
|
}
|
|
|
|
#endif
|
|
}
|
|
|
|
//development utility to take two snapshots and compare them for equivalence. This is used to test whether or not the new snapshot matches the original one that was the basis for compression
|
|
static void CompareSnapshot( const CFrameSnapshot* pOldSnapshot, const CFrameSnapshot* pNewSnapshot, int nChangePropTick )
|
|
{
|
|
//to avoid warnings when asserts aren't disabled
|
|
#ifdef DBGFLAG_ASSERT
|
|
|
|
//first off compare the valid entity list
|
|
CompareArray( pOldSnapshot->m_nValidEntities, pOldSnapshot->m_pValidEntities, pNewSnapshot->m_nValidEntities, pNewSnapshot->m_pValidEntities, sizeof( uint16 ) );
|
|
CompareArray( pOldSnapshot->m_iExplicitDeleteSlots.Count(), pOldSnapshot->m_iExplicitDeleteSlots.Base(), pNewSnapshot->m_iExplicitDeleteSlots.Count(), pNewSnapshot->m_iExplicitDeleteSlots.Base(), sizeof( int ) );
|
|
|
|
//now each element
|
|
for( int nCurrEntity = 0; nCurrEntity < pOldSnapshot->m_nValidEntities; ++nCurrEntity )
|
|
{
|
|
int nEntityIndex = pOldSnapshot->m_pValidEntities[ nCurrEntity ];
|
|
|
|
//get info about each entity and compare
|
|
const CFrameSnapshotEntry* pOldEntity = &pOldSnapshot->m_pEntities[ nEntityIndex ];
|
|
const CFrameSnapshotEntry* pNewEntity = &pNewSnapshot->m_pEntities[ nEntityIndex ];
|
|
|
|
Assert( pOldEntity->m_nSerialNumber == pNewEntity->m_nSerialNumber );
|
|
Assert( pOldEntity->m_pClass == pNewEntity->m_pClass );
|
|
Assert( ( pOldEntity->m_pPackedData != INVALID_PACKED_ENTITY_HANDLE ) == ( pNewEntity->m_pPackedData != INVALID_PACKED_ENTITY_HANDLE ) );
|
|
|
|
//now check the packed data if applicable
|
|
if( pOldEntity->m_pPackedData )
|
|
{
|
|
const PackedEntity* pOldPacked = framesnapshotmanager->GetPackedEntity( *( const_cast< CFrameSnapshot* >(pOldSnapshot) ), nEntityIndex );
|
|
const PackedEntity* pNewPacked = framesnapshotmanager->GetPackedEntity( *( const_cast< CFrameSnapshot* >(pNewSnapshot) ), nEntityIndex );
|
|
|
|
Assert( pOldPacked->m_nEntityIndex == pNewPacked->m_nEntityIndex );
|
|
Assert( pOldPacked->GetSnapshotCreationTick() == pNewPacked->GetSnapshotCreationTick() );
|
|
Assert( pOldPacked->ShouldCheckCreationTick() == pNewPacked->ShouldCheckCreationTick() );
|
|
Assert( pOldPacked->m_pServerClass == pNewPacked->m_pServerClass );
|
|
Assert( pOldPacked->m_pClientClass == pNewPacked->m_pClientClass );
|
|
|
|
//compare the recipients
|
|
CompareArray( pOldPacked->GetNumRecipients(), pOldPacked->GetRecipients(), pNewPacked->GetNumRecipients(), pNewPacked->GetRecipients(), sizeof( CSendProxyRecipients ) );
|
|
|
|
//compare the change list
|
|
{
|
|
int nNumProps = pOldPacked->GetChangeFrameList()->GetNumProps();
|
|
for( int nCurrProp = 0; nCurrProp < nNumProps; ++nCurrProp )
|
|
{
|
|
//see if this is a property that should have changed this frame (we have to do this since we lose some precision on the change flags during reconstruction)
|
|
if( pOldPacked->GetChangeFrameList()->GetPropTick( nCurrProp ) >= nChangePropTick )
|
|
{
|
|
Assert( pNewPacked->GetChangeFrameList()->GetPropTick( nCurrProp ) >= nChangePropTick );
|
|
}
|
|
}
|
|
}
|
|
|
|
//now compare properties
|
|
Assert( ( pOldPacked->GetPackedData() == SERIALIZED_ENTITY_HANDLE_INVALID ) == ( pNewPacked->GetPackedData() == SERIALIZED_ENTITY_HANDLE_INVALID ) );
|
|
if( pOldPacked->GetPackedData() != SERIALIZED_ENTITY_HANDLE_INVALID )
|
|
{
|
|
const CSerializedEntity* pOldProps = ( const CSerializedEntity* )pOldPacked->GetPackedData();
|
|
const CSerializedEntity* pNewProps = ( const CSerializedEntity* )pNewPacked->GetPackedData();
|
|
|
|
CompareArray( pOldProps->GetFieldCount(), pOldProps->GetFieldPaths(), pNewProps->GetFieldCount(), pNewProps->GetFieldPaths(), sizeof( short ) );
|
|
CompareArray( pOldProps->GetFieldCount(), pOldProps->GetFieldDataBitOffsets(), pNewProps->GetFieldCount(), pNewProps->GetFieldDataBitOffsets(), sizeof( int ) );
|
|
CompareArray( pOldProps->GetFieldDataBitCount() / 8, pOldProps->GetFieldData(), pNewProps->GetFieldDataBitCount() / 8, pNewProps->GetFieldData(), sizeof( uint8 ) );
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
size_t CHLTVServer::SHLTVDeltaFrame_t::GetMemSize()const
|
|
{
|
|
uint nSize = sizeof( *this );
|
|
if ( CFrameSnapshot *pSnapshot = m_pClientFrame->GetSnapshot() )
|
|
{
|
|
nSize += pSnapshot->GetMemSize();
|
|
}
|
|
return nSize;
|
|
}
|
|
|
|
size_t CFrameSnapshot::GetMemSize()const
|
|
{
|
|
size_t nSize = sizeof( *this );
|
|
nSize += m_nNumEntities * sizeof( CFrameSnapshotEntry ) + m_nValidEntities * sizeof( *m_pValidEntities );
|
|
return nSize;
|
|
}
|
|
|
|
void CHLTVServer::ExpandDeltaFrameToFullFrame( SHLTVDeltaFrame_t *pDeltaFrame )
|
|
{
|
|
//track the performance of this frame
|
|
VPROF_BUDGET( "CHLTVServer::ExpandDeltaFrameToFullFrame", "HLTV" );
|
|
|
|
CFrameSnapshot *pSnapshot = pDeltaFrame->m_pClientFrame->GetSnapshot();
|
|
|
|
//we need to construct our full entity list in our snapshot
|
|
pSnapshot->m_pEntities = new CFrameSnapshotEntry[ pDeltaFrame->m_nTotalEntities ];
|
|
pSnapshot->m_nNumEntities = pDeltaFrame->m_nTotalEntities;
|
|
|
|
//and create our valid entity list, which is just a one to one index map
|
|
pSnapshot->m_pValidEntities = new uint16 [ pDeltaFrame->m_nNumValidEntities ];
|
|
pSnapshot->m_nValidEntities = pDeltaFrame->m_nNumValidEntities;
|
|
|
|
//the index into our current previous frame so that we can find old versions of the object to delta against
|
|
const CFrameSnapshot* pPrevFrame = pDeltaFrame->m_pRelativeFrame;
|
|
const uint32 nPrevFrameEntities = ( pPrevFrame ) ? pPrevFrame->m_nNumEntities : 0;
|
|
|
|
//the time tick that we should use for all of our change flags. Since the HLTV server can skip frames, be conservative in our change
|
|
//tick settings and use the frame after our last frame if we have one (effectively collapsing change times backwards), and if not, just our current tick count
|
|
const int nChangePropTick = ( pPrevFrame ) ? pPrevFrame->m_nTickCount + 1 : pSnapshot->m_nTickCount;
|
|
|
|
//the bit list that indicates which entities to just copy forward
|
|
const uint32* RESTRICT pCopyEntities = pDeltaFrame->m_pCopyEntities;
|
|
const uint32 nTotalEntities = pDeltaFrame->m_nTotalEntities;
|
|
|
|
SHLTVDeltaEntity_t *pCurrDeltaEntity = pDeltaFrame->m_pEntities;
|
|
uint16* RESTRICT pCurrOutValidEntity = pSnapshot->m_pValidEntities;
|
|
|
|
//run through each of our valid entities
|
|
for( uint32 nCurrEntity = 0; nCurrEntity < nTotalEntities; ++nCurrEntity )
|
|
{
|
|
CFrameSnapshotEntry *pCurrEntity = pSnapshot->m_pEntities + nCurrEntity;
|
|
|
|
//see if we just need to copy this forward
|
|
if( pCopyEntities[ nCurrEntity / 32 ] & ( 1 << ( nCurrEntity % 32 ) ) )
|
|
{
|
|
const CFrameSnapshotEntry *pPrevEntity = &pPrevFrame->m_pEntities[ nCurrEntity ];
|
|
|
|
//all we need to do is copy the previous to the current
|
|
pCurrEntity->m_nSerialNumber = pPrevEntity->m_nSerialNumber;
|
|
pCurrEntity->m_pClass = pPrevEntity->m_pClass;
|
|
pCurrEntity->m_pPackedData = pPrevEntity->m_pPackedData;
|
|
|
|
//and make sure to reference our packed data so it won't go away on us
|
|
if( pCurrEntity->m_pPackedData != INVALID_PACKED_ENTITY_HANDLE )
|
|
{
|
|
PackedEntity* pPacked = framesnapshotmanager->GetPackedEntity( pCurrEntity->m_pPackedData );
|
|
pPacked->m_ReferenceCount++;
|
|
}
|
|
|
|
//and add this entity to the valid entity list
|
|
*pCurrOutValidEntity = ( uint16 )nCurrEntity;
|
|
pCurrOutValidEntity++;
|
|
}
|
|
//otherwise, see if it is our delta entity that we encoded
|
|
else if( pCurrDeltaEntity && ( nCurrEntity == pCurrDeltaEntity->m_nSourceIndex ) )
|
|
{
|
|
//we have matched this delta entity, so move onto the next
|
|
SHLTVDeltaEntity_t* pDeltaEntity = pCurrDeltaEntity;
|
|
pCurrDeltaEntity = pCurrDeltaEntity->m_pNext;
|
|
|
|
//setup our valid list to index into this slot
|
|
*pCurrOutValidEntity = ( uint16 )nCurrEntity;
|
|
pCurrOutValidEntity++;
|
|
|
|
//copy over the raw data
|
|
pCurrEntity->m_nSerialNumber = pDeltaEntity->m_nSerialNumber;
|
|
pCurrEntity->m_pClass = pDeltaEntity->m_pServerClass;
|
|
pCurrEntity->m_pPackedData = INVALID_PACKED_ENTITY_HANDLE;
|
|
|
|
//see if there is no packed data associated with this class
|
|
if( pDeltaEntity->m_SerializedEntity == SHLTVDeltaEntity_t::knNoPackedData )
|
|
continue;
|
|
|
|
//see if we can find a previous entity in order to diff ourself against
|
|
PackedEntityHandle_t PrevPackedHandle = INVALID_PACKED_ENTITY_HANDLE;
|
|
PackedEntity *pPrevPacked = NULL;
|
|
|
|
if( nCurrEntity < nPrevFrameEntities )
|
|
{
|
|
const CFrameSnapshotEntry *pPrevEntity = &pPrevFrame->m_pEntities[ nCurrEntity ];
|
|
if( ( pPrevEntity->m_nSerialNumber == pDeltaEntity->m_nSerialNumber ) && ( pPrevEntity->m_pClass == pDeltaEntity->m_pServerClass ) )
|
|
{
|
|
PrevPackedHandle = pPrevEntity->m_pPackedData;
|
|
if( PrevPackedHandle != INVALID_PACKED_ENTITY_HANDLE )
|
|
pPrevPacked = framesnapshotmanager->GetPackedEntity( PrevPackedHandle );
|
|
}
|
|
}
|
|
|
|
//now the packed entity contents
|
|
PackedEntity* pNewPacked = framesnapshotmanager->CreateLocalPackedEntity( pSnapshot, pDeltaEntity->m_nSourceIndex );
|
|
|
|
pNewPacked->SetServerAndClientClass( pDeltaEntity->m_pServerClass, NULL );
|
|
pNewPacked->SetSnapshotCreationTick( pDeltaEntity->m_nSnapshotCreationTick );
|
|
|
|
//update our entities (which we either have stored, or we need to take from the previous frame)
|
|
if( pDeltaEntity->m_pNewRecipients )
|
|
{
|
|
pNewPacked->SetRecipients( CUtlMemory< CSendProxyRecipients >( pDeltaEntity->m_pNewRecipients, pDeltaEntity->m_nNumRecipients ) );
|
|
}
|
|
else if( pDeltaEntity->m_nNumRecipients > 0 )
|
|
{
|
|
//sanity check that they didn't change, and that we have valid frame of reference
|
|
Assert( pPrevPacked && ( pPrevPacked->GetNumRecipients() == pDeltaEntity->m_nNumRecipients ) );
|
|
pNewPacked->SetRecipients( CUtlMemory< CSendProxyRecipients >( pPrevPacked->GetRecipients(), pPrevPacked->GetNumRecipients() ) );
|
|
}
|
|
|
|
//and handle expanding out our serialized values. Either we can have a new object (just steal the properties), no changes (copy the original properties) or a diff
|
|
//(merge the properties)
|
|
if( !pPrevPacked )
|
|
{
|
|
//creation, so just use ours
|
|
pNewPacked->SetPackedData( pDeltaEntity->m_SerializedEntity );
|
|
pDeltaEntity->m_SerializedEntity = SERIALIZED_ENTITY_HANDLE_INVALID;
|
|
//setup a change list for all of our possible properties that is set to our creation time
|
|
CChangeFrameList* pChangeList = new CChangeFrameList( SendTable_GetNumFlatProps( pDeltaEntity->m_pServerClass->m_pTable ), pDeltaEntity->m_nSnapshotCreationTick );
|
|
pNewPacked->SetChangeFrameList( pChangeList );
|
|
}
|
|
else
|
|
{
|
|
const CSerializedEntity* pPrevProps = ( const CSerializedEntity* )pPrevPacked->GetPackedData();
|
|
|
|
//copy over our base change list so we can update the times that the properties changed
|
|
CChangeFrameList* pChangeList = new CChangeFrameList( *pPrevPacked->GetChangeFrameList() );
|
|
pNewPacked->SetChangeFrameList( pChangeList );
|
|
|
|
//if we don't have a serialized entity, we can just copy our previous (much faster)
|
|
if(pDeltaEntity->m_SerializedEntity == SERIALIZED_ENTITY_HANDLE_INVALID)
|
|
{
|
|
CSerializedEntity* pNewProps = new CSerializedEntity( );
|
|
pNewProps->Copy( *pPrevProps );
|
|
pNewPacked->SetPackedData( ( SerializedEntityHandle_t )pNewProps );
|
|
}
|
|
else
|
|
{
|
|
//we have to merge our new values onto our old ones
|
|
CSerializedEntity* pDeltaProps = ( CSerializedEntity* )pDeltaEntity->m_SerializedEntity;
|
|
BuildMergedPropertySet( pDeltaProps, pPrevProps, pChangeList, nChangePropTick );
|
|
//transfer ownership of our properties over to this object
|
|
pNewPacked->SetPackedData( ( SerializedEntityHandle_t ) pDeltaProps );
|
|
pDeltaEntity->m_SerializedEntity = SERIALIZED_ENTITY_HANDLE_INVALID;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//we don't have this object on this frame, so clear it out
|
|
pCurrEntity->m_nSerialNumber = -1;
|
|
pCurrEntity->m_pClass = NULL;
|
|
pCurrEntity->m_pPackedData = INVALID_PACKED_ENTITY_HANDLE;
|
|
}
|
|
}
|
|
|
|
//Sanity check our expansion
|
|
if( pDeltaFrame->m_pSourceFrame )
|
|
CompareSnapshot( pDeltaFrame->m_pSourceFrame, pDeltaFrame->m_pClientFrame->GetSnapshot(), nChangePropTick );
|
|
}
|
|
|
|
void CHLTVServer::ExpandDeltaFramesToTick( int nTick )
|
|
{
|
|
//just expand and add all frames until we find one that is of a later tick
|
|
while ( m_pOldestDeltaFrame )
|
|
{
|
|
SHLTVDeltaFrame_t *pFrame = m_pOldestDeltaFrame;
|
|
|
|
//see if our oldest is still too new to decompress (-1 means decompress them all)
|
|
if( ( nTick != -1 ) && ( pFrame->m_pClientFrame->tick_count > nTick ) )
|
|
break;
|
|
|
|
//expand the frame
|
|
ExpandDeltaFrameToFullFrame( pFrame );
|
|
|
|
//now add this into our frame list
|
|
AddClientFrame( pFrame->m_pClientFrame );
|
|
|
|
//give up ownership of it since someone else is now holding onto it
|
|
pFrame->m_pClientFrame = NULL;
|
|
|
|
//remove this frame from our list
|
|
m_pOldestDeltaFrame = m_pOldestDeltaFrame->m_pNewerDeltaFrame;
|
|
if( m_pOldestDeltaFrame == NULL )
|
|
m_pNewestDeltaFrame = NULL;
|
|
|
|
//and nuke the memory
|
|
delete pFrame;
|
|
}
|
|
}
|
|
|
|
void CHLTVServer::FreeAllDeltaFrames( )
|
|
{
|
|
while( m_pOldestDeltaFrame )
|
|
{
|
|
//advance to the next list entry
|
|
SHLTVDeltaFrame_t* pCurrFrame = m_pOldestDeltaFrame;
|
|
m_pOldestDeltaFrame = pCurrFrame->m_pNewerDeltaFrame;
|
|
|
|
//free everything about this frame
|
|
delete pCurrFrame;
|
|
}
|
|
|
|
//and make sure to completely reset our list
|
|
m_pNewestDeltaFrame = NULL;
|
|
}
|
|
|
|
|
|
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("GOTV relay active (%d)\n", GetInstanceIndex() ); // there should only be one relay
|
|
|
|
Steam3Server().Activate();
|
|
Steam3Server().SendUpdatedServerDetails();
|
|
|
|
if ( serverGameDLL )
|
|
{
|
|
serverGameDLL->GameServerSteamAPIActivated( true );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ConMsg("GOTV[%d] broadcast active.\n", GetInstanceIndex() );
|
|
}
|
|
}
|
|
|
|
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
|
|
int nClientFrameCount = AddClientFrame( hltvFrame );
|
|
|
|
// Only keep the number of packets required to satisfy tv_delay at our tv snapshot rate
|
|
static ConVarRef tv_delay( "tv_delay" );
|
|
|
|
float flTvDelayToKeep = tv_delay.GetFloat();
|
|
if ( spec_replay_enable.GetBool() )
|
|
flTvDelayToKeep = Max( flTvDelayToKeep, spec_replay_message_time.GetFloat() + spec_replay_leadup_time.GetFloat() );
|
|
|
|
extern ConVar tv_snapshotrate;
|
|
int numFramesToKeep = 2 * ( ( 1 + Max( 1.0f, flTvDelayToKeep ) ) * int( m_flSnapshotRate ) );
|
|
if ( numFramesToKeep < MAX_CLIENT_FRAMES )
|
|
numFramesToKeep = MAX_CLIENT_FRAMES;
|
|
while ( nClientFrameCount > numFramesToKeep )
|
|
{
|
|
RemoveOldestFrame();
|
|
-- nClientFrameCount;
|
|
}
|
|
|
|
// reset HLTV frame for recording next messages etc.
|
|
m_HLTVFrame.Reset();
|
|
m_HLTVFrame.SetSnapshot( NULL );
|
|
|
|
return hltvFrame;
|
|
}
|
|
|
|
|
|
|
|
// Different HLTV servers may have e.g. different snapshot rates.
|
|
// This is a chance for HLTV server to patch up some convars for its clients (like the tv_snapshotrate)
|
|
void CHLTVServer::FixupConvars( CNETMsg_SetConVar_t &convars )
|
|
{
|
|
if ( GetSnapshotRate() != tv_snapshotrate.GetFloat() )
|
|
{
|
|
char rate[ 32 ];
|
|
V_snprintf( rate, sizeof( rate ), "%g", GetSnapshotRate() );
|
|
bool bReplaced = false;
|
|
for ( int i = 0; i < convars.convars().cvars_size(); ++i )
|
|
{
|
|
if ( convars.convars().cvars( i ).name() == "tv_snapshotrate" )
|
|
{
|
|
convars.mutable_convars()->mutable_cvars( i )->set_value( rate );
|
|
bReplaced = true;
|
|
break;
|
|
}
|
|
}
|
|
if ( !bReplaced )
|
|
{
|
|
CMsg_CVars_CVar *pCVar = convars.mutable_convars()->add_cvars();
|
|
pCVar->set_name( "tv_snapshotrate" );
|
|
pCVar->set_value( rate );
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
|
|
bool CHLTVServer::SendClientMessages( CHLTVClient *client )
|
|
{
|
|
// Update Host client send state...
|
|
if ( !client->ShouldSendMessages() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 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;
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
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", "#GOTV_Reconnecting" );
|
|
}
|
|
else
|
|
{
|
|
int proxies = 0, slots = 0, clients = 0;
|
|
|
|
for ( CActiveHltvServerIterator hltv; hltv; hltv.Next() )
|
|
{
|
|
int nLocalProxyCount, nLocalSlotCount, nLocalClientCount;
|
|
hltv->GetGlobalStats( nLocalProxyCount,nLocalSlotCount,nLocalClientCount );
|
|
proxies += nLocalProxyCount;
|
|
slots += nLocalSlotCount;
|
|
clients += nLocalClientCount;
|
|
}
|
|
|
|
event = g_GameEventManager.CreateEvent( "hltv_status", true );
|
|
|
|
if ( !event )
|
|
return;
|
|
|
|
//
|
|
// There's no reason to ever record IP addresses in GOTV demo
|
|
//
|
|
// 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 );
|
|
|
|
int numExternalTotalViewers, numExternalLinkedViewers;
|
|
GetExternalStats( numExternalTotalViewers, numExternalLinkedViewers );
|
|
event->SetInt( "externaltotal", numExternalTotalViewers );
|
|
event->SetInt( "externallinked", numExternalLinkedViewers );
|
|
}
|
|
|
|
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::NETMsg_PlayerAvatarData( const CNETMsg_PlayerAvatarData& msg )
|
|
{
|
|
PlayerAvatarDataMap_t::IndexType_t idxData = m_mapPlayerAvatarData.Find( msg.accountid() );
|
|
if ( idxData != m_mapPlayerAvatarData.InvalidIndex() )
|
|
{
|
|
delete m_mapPlayerAvatarData.Element( idxData );
|
|
m_mapPlayerAvatarData.RemoveAt( idxData );
|
|
}
|
|
|
|
CNETMsg_PlayerAvatarData_t *pHtlvDataCopy = new CNETMsg_PlayerAvatarData_t;
|
|
pHtlvDataCopy->CopyFrom( msg );
|
|
m_mapPlayerAvatarData.Insert( pHtlvDataCopy->accountid(), pHtlvDataCopy );
|
|
|
|
// Enqueue this message for all fully connected clients immediately
|
|
for ( int iClient = 0; iClient < GetClientCount(); ++iClient )
|
|
{
|
|
CBaseClient *pClient = dynamic_cast< CBaseClient * >( GetClient( iClient ) );
|
|
if ( !pClient->IsActive() )
|
|
continue;
|
|
|
|
if ( INetChannel *pNetChannel = pClient->GetNetChannel() )
|
|
{
|
|
pNetChannel->EnqueueVeryLargeAsyncTransfer( *pHtlvDataCopy );
|
|
}
|
|
}
|
|
|
|
if ( m_DemoRecorder.IsRecording() )
|
|
{
|
|
m_DemoRecorder.RecordPlayerAvatar( pHtlvDataCopy );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CHLTVServer::SendNetMsg( INetMessage &msg, bool bForceReliable, bool bVoice )
|
|
{
|
|
//
|
|
// When sending messages to HLTV client we encrypt some messages with encryption key
|
|
//
|
|
if ( serverGameDLL &&
|
|
( *tv_encryptdata_key.GetString() || *tv_encryptdata_key_pub.GetString() ) &&
|
|
( msg.GetType() != svc_EncryptedData ) )
|
|
{
|
|
EncryptedMessageKeyType_t eKeyType = serverGameDLL->GetMessageEncryptionKey( &msg );
|
|
char const *szEncryptionKey = "";
|
|
switch ( eKeyType )
|
|
{
|
|
case kEncryptedMessageKeyType_Private:
|
|
szEncryptionKey = tv_encryptdata_key.GetString();
|
|
break;
|
|
case kEncryptedMessageKeyType_Public:
|
|
szEncryptionKey = tv_encryptdata_key_pub.GetString();
|
|
break;
|
|
}
|
|
if ( szEncryptionKey && *szEncryptionKey )
|
|
{
|
|
CSVCMsg_EncryptedData_t encryptedMessage;
|
|
if ( !CmdEncryptedDataMessageCodec::SVCMsg_EncryptedData_EncryptMessage( encryptedMessage, &msg, szEncryptionKey ) )
|
|
return false;
|
|
encryptedMessage.set_key_type( eKeyType );
|
|
return SendNetMsg( encryptedMessage, true, false ); // recurse and send the generated messages as reliable
|
|
}
|
|
}
|
|
|
|
//
|
|
// Special message handling for avatar data
|
|
//
|
|
if ( msg.GetType() == net_PlayerAvatarData )
|
|
{
|
|
CNETMsg_PlayerAvatarData const *pPlayerAvatarData = dynamic_cast< CNETMsg_PlayerAvatarData * >( &msg );
|
|
if ( !pPlayerAvatarData )
|
|
return false;
|
|
|
|
return NETMsg_PlayerAvatarData( *pPlayerAvatarData );
|
|
}
|
|
|
|
//
|
|
// Send the actual outgoing message
|
|
//
|
|
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
|
|
|
|
AUTO_LOCK_FM( m_FrameCacheMutex ); // we need to lock frame cache because we're potentially modifying it from multiple sendPacket threads
|
|
// 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;
|
|
}
|
|
|
|
|
|
CClientFrame *CHLTVServer::ExpandAndGetClientFrame( int nTick, bool bExact )
|
|
{
|
|
ExpandDeltaFramesToTick( nTick );
|
|
return GetClientFrame( nTick, bExact );
|
|
}
|
|
|
|
|
|
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() );
|
|
#if HLTV_REPLAY_ENABLED
|
|
if ( spec_replay_enable.GetBool() )
|
|
{
|
|
nNewTick = Max< int >( nNewTick, m_nLastTick - ( spec_replay_message_time.GetFloat() + spec_replay_leadup_time.GetFloat() ) / m_flTickInterval );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//handle expanding any delta frames we have accumulated up to this point
|
|
ExpandDeltaFramesToTick( nNewTick );
|
|
|
|
// 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 );
|
|
}
|
|
|
|
if ( ( m_DemoRecorder.IsRecording() || m_Broadcast.IsRecording() ) && m_CurrentFrame )
|
|
{
|
|
if ( m_DemoRecorder.IsRecording() )
|
|
m_DemoRecorder.WriteFrame( m_CurrentFrame, &m_DemoEventWriteBuffer );
|
|
if ( m_Broadcast.IsRecording() )
|
|
m_Broadcast.WriteFrame( m_CurrentFrame, &m_DemoEventWriteBuffer );
|
|
m_DemoEventWriteBuffer.Reset();
|
|
}
|
|
}
|
|
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 - tv_window_size.GetFloat() / 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(CSVCMsg_ServerInfo &serverinfo)
|
|
{
|
|
CBaseServer::FillServerInfo( serverinfo );
|
|
|
|
serverinfo.set_player_slot( m_nPlayerSlot ); // all spectators think they're the HLTV client
|
|
serverinfo.set_max_clients( 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_nPlayerSlot = 0;
|
|
m_flStartTime = 0.0f;
|
|
m_nViewEntity = 1;
|
|
m_nGameServerMaxClients = 0;
|
|
m_fNextSendUpdateTime = 0.0f;
|
|
|
|
Changelevel( false );
|
|
}
|
|
|
|
void CHLTVServer::Init(bool bIsDedicated)
|
|
{
|
|
CBaseServer::Init( bIsDedicated );
|
|
|
|
m_Socket = NS_HLTV + m_nInstanceIndex;
|
|
COMPILE_TIME_ASSERT(NS_HLTV1 == NS_HLTV + 1 );
|
|
|
|
// check if only master proxy is allowed, no broadcasting
|
|
if ( CommandLine()->FindParm("-tvmasteronly") )
|
|
{
|
|
m_bMasterOnlyMode = true;
|
|
}
|
|
}
|
|
|
|
void CHLTVServer::Changelevel( bool bInactivateClients )
|
|
{
|
|
m_Broadcast.StopRecording();// We can't broadcast after level change, because the broadcast manifest (which includes map name) is immutable during broadcast.
|
|
StopRecordingAndFreeFrames( true );
|
|
|
|
if ( bInactivateClients )
|
|
{
|
|
InactivateClients();
|
|
}
|
|
|
|
m_CurrentFrame = NULL;
|
|
|
|
m_HLTVFrame.FreeBuffers();
|
|
m_vPVSOrigin.Init();
|
|
|
|
DeleteClientFrames( -1 );
|
|
|
|
m_DeltaCache.Flush();
|
|
m_FrameCache.RemoveAll();
|
|
|
|
|
|
//free any frames that we may have had outstanding
|
|
FreeAllDeltaFrames();
|
|
|
|
//release any snapshots that we were referencing for delta frame construction
|
|
if( m_pLastSourceSnapshot )
|
|
{
|
|
m_pLastSourceSnapshot->ReleaseReference();
|
|
m_pLastSourceSnapshot = NULL;
|
|
}
|
|
if( m_pLastTargetSnapshot )
|
|
{
|
|
m_pLastTargetSnapshot->ReleaseReference();
|
|
m_pLastTargetSnapshot = NULL;
|
|
}
|
|
|
|
// Free all avatar data
|
|
m_mapPlayerAvatarData.PurgeAndDeleteElements();
|
|
}
|
|
|
|
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_nExternalTotalViewers = 0;
|
|
m_nExternalLinkedViewers = 0;
|
|
|
|
//stop any recording, and free our client frame list
|
|
m_Broadcast.StopRecording();
|
|
StopRecordingAndFreeFrames( true );
|
|
UninstallStringTables();
|
|
|
|
if ( IsMasterProxy() )
|
|
{
|
|
if ( m_MasterClient )
|
|
m_MasterClient->Disconnect( "GOTV stop." );
|
|
|
|
if ( m_Director )
|
|
m_Director->RemoveHLTVServer( this );
|
|
}
|
|
else
|
|
{
|
|
// do not try to reconnect to old connection
|
|
m_ClientState.m_Remote.RemoveAll();
|
|
|
|
m_ClientState.Disconnect();
|
|
}
|
|
|
|
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 )const
|
|
{
|
|
return m_bPlayingBack;
|
|
}
|
|
|
|
bool CHLTVServer::IsPlaybackPaused()const
|
|
{
|
|
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;
|
|
}
|
|
|
|
|
|
bool CHLTVServer::StartPlayback( const char *filename, bool bAsTimeDemo, CDemoPlaybackParameters_t const *pPlaybackParameters, int nStartingTick )
|
|
{
|
|
Clear();
|
|
|
|
if ( !m_DemoFile.Open( filename, true ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Read in the m_DemoHeader
|
|
demoheader_t *dh = m_DemoFile.ReadDemoHeader( pPlaybackParameters );
|
|
|
|
if ( !dh )
|
|
{
|
|
ConMsg( "Failed to read demo header.\n" );
|
|
m_DemoFile.Close();
|
|
return false;
|
|
}
|
|
|
|
// create a fake channel with a NULL address (no encryption)
|
|
m_ClientState.m_NetChannel = NET_CreateNetChannel( NS_CLIENT, NULL, "DEMO", &m_ClientState, NULL, false );
|
|
|
|
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; 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.SetAddrType( NSAT_NETADR );
|
|
demoPacket.from.AsType<netadr_t>().SetType( NA_LOOPBACK);
|
|
|
|
while ( true )
|
|
{
|
|
int nPlayerSlot = 0;
|
|
m_DemoFile.ReadCmdHeader( cmd, tick, nPlayerSlot );
|
|
|
|
// COMMAND HANDLERS
|
|
switch ( cmd )
|
|
{
|
|
case dem_synctick:
|
|
ResyncDemoClock();
|
|
break;
|
|
case dem_stop:
|
|
// MOTODO we finished reading the file
|
|
return ;
|
|
case dem_consolecmd:
|
|
{
|
|
#ifndef DEDICATED
|
|
ACTIVE_SPLITSCREEN_PLAYER_GUARD( nPlayerSlot );
|
|
#endif
|
|
CNETMsg_StringCmd_t cmdmsg( m_DemoFile.ReadConsoleCommand() );
|
|
m_ClientState.NETMsg_StringCmd( 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 = malloc( 512*1024 ); // X360TBD: How much memory is really needed here?
|
|
bf_read buf( "dem_stringtables", data, 512*1024 );
|
|
|
|
m_DemoFile.ReadStringTables( &buf );
|
|
buf.Seek( 0 );
|
|
|
|
if ( !networkStringTableContainerClient->ReadStringTables( buf ) )
|
|
{
|
|
Host_Error( "Error parsing string tables during demo playback." );
|
|
}
|
|
|
|
free( data );
|
|
}
|
|
break;
|
|
case dem_usercmd:
|
|
{
|
|
#ifndef DEDICATED
|
|
ACTIVE_SPLITSCREEN_PLAYER_GUARD( nPlayerSlot );
|
|
#endif
|
|
char buffer[256];
|
|
int length = sizeof(buffer);
|
|
m_DemoFile.ReadUserCmd( buffer, 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
#define DEBUG_GOTV_RELAY_LOCAL 0
|
|
#if DEBUG_GOTV_RELAY_LOCAL
|
|
ConVar debug_gotv_relay_whitelist_ports( "debug_gotv_relay_whitelist_ports", "[27005][27006][27007]" ); // run 3 game servers first
|
|
#endif
|
|
int CHLTVServer::GetChallengeType ( const ns_address &adr )
|
|
{
|
|
if ( serverGameDLL && serverGameDLL->IsValveDS() )
|
|
{
|
|
#if DEBUG_GOTV_RELAY_LOCAL
|
|
if ( strstr( debug_gotv_relay_whitelist_ports.GetString(), CFmtStr( "[%u]", adr.GetPort() ) ) )
|
|
return PROTOCOL_HASHEDCDKEY; // When debugging on local machine only make exception for relay proxies, clients actually auth via SteamID
|
|
#else
|
|
extern bool IsHltvRelayProxyWhitelisted( ns_address const &adr );
|
|
if ( IsHltvRelayProxyWhitelisted( adr ) )
|
|
return PROTOCOL_HASHEDCDKEY; // HLTV makes an exception for the requesting relay proxy address
|
|
#endif
|
|
|
|
return CBaseServer::GetChallengeType( adr );
|
|
}
|
|
|
|
return PROTOCOL_HASHEDCDKEY; // HLTV doesn't need Steam authentication
|
|
}
|
|
|
|
static bool Helper_HLTV_VerifyOfficialPassword( const char *szPassword )
|
|
{
|
|
if ( !szPassword || !szPassword[0] )
|
|
return false;
|
|
if ( Q_strlen( szPassword ) != 32 )
|
|
return false;
|
|
char chSignHash[16] = {0};
|
|
Q_snprintf( chSignHash, ARRAYSIZE( chSignHash ), "%08lX", CRC32_ProcessSingleBuffer( szPassword, 24 ) );
|
|
return ( 0 == Q_strncmp( chSignHash, szPassword + 24, 8 ) );
|
|
}
|
|
|
|
static char const *Helper_HLTV_GenerateUniquePassword()
|
|
{
|
|
return "HLTV Official Password Must Be Encrypted";
|
|
}
|
|
|
|
#if 0
|
|
CON_COMMAND( debug_make_hltv_encrypted_password, "" )
|
|
{
|
|
char const *szPasswordProvidedByClient = args.Arg( 1 );
|
|
if ( !szPasswordProvidedByClient || !*szPasswordProvidedByClient || ( Q_strlen( szPasswordProvidedByClient ) != 32 ) )
|
|
{
|
|
Warning( "Bad password!\n" );
|
|
return;
|
|
}
|
|
|
|
char chClientHash[64]={0};
|
|
Q_snprintf( chClientHash, ARRAYSIZE( chClientHash ), "%08X%08X%08X",
|
|
CRC32_ProcessSingleBuffer( szPasswordProvidedByClient, 32 ),
|
|
CRC32_ProcessSingleBuffer( szPasswordProvidedByClient + 10, 22 ),
|
|
CRC32_ProcessSingleBuffer( szPasswordProvidedByClient + 20, 12 ) );
|
|
Q_snprintf( chClientHash + 24, ARRAYSIZE( chClientHash ) - 24, "%08X",
|
|
CRC32_ProcessSingleBuffer( chClientHash, 24 ) );
|
|
|
|
Msg( "{%s}->{%s}\n", szPasswordProvidedByClient, chClientHash );
|
|
}
|
|
#endif
|
|
|
|
bool CHLTVServer::CheckHltvPasswordMatch( const char *szPasswordProvidedByClient, const char *szServerRequiredPassword, CSteamID steamidClient )
|
|
{
|
|
// Official servers must have a special encrypted password
|
|
if ( serverGameDLL && serverGameDLL->IsValveDS() )
|
|
{
|
|
if ( !Helper_HLTV_VerifyOfficialPassword( szServerRequiredPassword ) )
|
|
{
|
|
ExecuteNTimes( 3, Warning( "WARNING: %s (%s)\n", Helper_HLTV_GenerateUniquePassword(), szServerRequiredPassword ? szServerRequiredPassword : "none" ) );
|
|
return false; // without the encrypted password clients cannot connect
|
|
}
|
|
|
|
if ( !szPasswordProvidedByClient || !szPasswordProvidedByClient[0] )
|
|
return false; // server requires a password, but client didn't provide a password
|
|
|
|
// Enforce client password length to be 32 characters
|
|
if ( Q_strlen( szPasswordProvidedByClient ) == 32 )
|
|
{
|
|
// Compute a client password hash
|
|
char chClientHash[64]={0};
|
|
Q_snprintf( chClientHash, ARRAYSIZE( chClientHash ), "%08lX%08lX%08lX",
|
|
CRC32_ProcessSingleBuffer( szPasswordProvidedByClient, 32 ),
|
|
CRC32_ProcessSingleBuffer( szPasswordProvidedByClient + 10, 22 ),
|
|
CRC32_ProcessSingleBuffer( szPasswordProvidedByClient + 20, 12 ) );
|
|
if ( !Q_strncmp( chClientHash, szServerRequiredPassword, 24 ) )
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( !szServerRequiredPassword || !szServerRequiredPassword[0] || !Q_stricmp( szServerRequiredPassword, "none" ) )
|
|
return true; // server doesn't require a password, allow client
|
|
if ( !szPasswordProvidedByClient || !szPasswordProvidedByClient[0] )
|
|
return false; // server requires a password, but client didn't provide a password
|
|
if ( !Q_strcmp( szPasswordProvidedByClient, szServerRequiredPassword ) )
|
|
return true; // compare passwords
|
|
}
|
|
|
|
//
|
|
// Check if client is connecting via watchable reservation
|
|
//
|
|
if ( tv_advertise_watchable.GetBool() && sv.IsReserved() && sv.GetReservationCookie() &&
|
|
szPasswordProvidedByClient && ( Q_strlen( szPasswordProvidedByClient ) == 64 ) )
|
|
{
|
|
// Decode client TV watchable password ascii
|
|
unsigned char chEncryptedPassword[ 32 + 1 ] = {0};
|
|
for ( int k = 0; k < 32; ++ k )
|
|
{
|
|
char chScan[5] = { '0', 'x', szPasswordProvidedByClient[2*k], szPasswordProvidedByClient[2*k+1], 0 };
|
|
uint32 uiByte = 0;
|
|
sscanf( chScan, "0x%02X", &uiByte );
|
|
chEncryptedPassword[k] = uiByte;
|
|
}
|
|
|
|
// Decrypt the encoded password
|
|
IceKey iceKey( 2 );
|
|
if ( iceKey.keySize() == 16 )
|
|
{
|
|
iceKey.set( ( unsigned char * ) CFmtStr( "%016llX", sv.GetReservationCookie() ).Access() );
|
|
char chDecryptedPassword[ 32 + 1 ] = {0};
|
|
for ( int k = 0; k < 32; k += iceKey.blockSize() )
|
|
{
|
|
iceKey.decrypt( chEncryptedPassword + k, ( unsigned char * ) chDecryptedPassword + k );
|
|
}
|
|
if ( !Q_strcmp( chDecryptedPassword, CFmtStr( "WATCH100%08X%016llX", steamidClient.GetAccountID(), sv.GetReservationCookie() ).Access() ) )
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const char *CHLTVServer::GetPassword() const
|
|
{
|
|
const char *password = tv_password.GetString();
|
|
|
|
// Official servers must have a special encrypted password
|
|
if ( serverGameDLL && serverGameDLL->IsValveDS() && !Helper_HLTV_VerifyOfficialPassword( password ) )
|
|
return Helper_HLTV_GenerateUniquePassword();
|
|
|
|
// if password is empty or "none", return NULL
|
|
if ( !password[0] || !Q_stricmp(password, "none" ) )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
return password;
|
|
}
|
|
|
|
const char *CHLTVServer::GetHltvRelayPassword() const
|
|
{
|
|
static ConVarRef tv_relaypassword( "tv_relaypassword" );
|
|
const char *password = tv_relaypassword.GetString();
|
|
|
|
if ( serverGameDLL && serverGameDLL->IsValveDS() && !Helper_HLTV_VerifyOfficialPassword( password ) )
|
|
return Helper_HLTV_GenerateUniquePassword();
|
|
|
|
// if password is empty or "none", return NULL
|
|
if ( !password[0] || !Q_stricmp(password, "none" ) )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
return password;
|
|
}
|
|
|
|
IClient *CHLTVServer::ConnectClient ( const ns_address &adr, int protocol, int challenge, int authProtocol,
|
|
const char *name, const char *password, const char *hashedCDkey, int cdKeyLen,
|
|
CUtlVector< CCLCMsg_SplitPlayerConnect_t * > & splitScreenClients, bool isClientLowViolence, CrossPlayPlatform_t clientPlatform,
|
|
const byte *pbEncryptionKey, int nEncryptionKeyIndex )
|
|
{
|
|
IClient *client = (CHLTVClient*)CBaseServer::ConnectClient(
|
|
adr, protocol, challenge,authProtocol, name, password, hashedCDkey, cdKeyLen, splitScreenClients, isClientLowViolence, clientPlatform,
|
|
pbEncryptionKey, nEncryptionKeyIndex );
|
|
|
|
if ( client )
|
|
{
|
|
// remember password
|
|
CHLTVClient *pHltvClient = (CHLTVClient*)client;
|
|
Q_strncpy( pHltvClient->m_szPassword, password, sizeof(pHltvClient->m_szPassword) );
|
|
}
|
|
|
|
return client;
|
|
}
|
|
|
|
bool CHLTVServer::GetRedirectAddressForConnectClient( const ns_address &adr, CUtlVector< CCLCMsg_SplitPlayerConnect_t* > & splitScreenClients, ns_address *pNetAdrRedirect )
|
|
{
|
|
bool bConnectingClientIsTvRelay = false;
|
|
|
|
if ( splitScreenClients.Count() )
|
|
{
|
|
const CMsg_CVars& convars = splitScreenClients[0]->convars();
|
|
for ( int i = 0; i< convars.cvars_size(); ++i )
|
|
{
|
|
const char *cvname = NetMsgGetCVarUsingDictionary( convars.cvars(i) );
|
|
const char *value = convars.cvars(i).value().c_str();
|
|
|
|
if ( stricmp( cvname, "tv_relay" ) )
|
|
continue;
|
|
|
|
bConnectingClientIsTvRelay = ( value[0] == '1' );
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( bConnectingClientIsTvRelay )
|
|
return false;
|
|
|
|
// This is a human spectator, check if we should dispatch them down the chain
|
|
//
|
|
// The section below is largely a copy of DispatchToRelay function
|
|
//
|
|
|
|
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() )
|
|
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
|
|
pNetAdrRedirect->Clear();
|
|
RejectConnection( adr, "No GOTV relay available" );
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// just let client connect to this proxy
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// check if client should stay on this relay server unless we are the master,
|
|
// masters always prefer to send clients to relays
|
|
if ( (tv_dispatchmode.GetInt() == DISPATCH_MODE_AUTO) && (GetMaxClients() > 0) )
|
|
{
|
|
// ratio = clients/slots. give relay proxies 25% bonus
|
|
int numSlots = GetMaxClients();
|
|
if ( tv_maxclients_relayreserved.GetInt() > 0 )
|
|
numSlots -= tv_maxclients_relayreserved.GetInt();
|
|
numSlots = MAX( 0, numSlots );
|
|
|
|
int numClients = GetNumClients();
|
|
if ( numClients > numSlots )
|
|
numSlots = numClients;
|
|
|
|
float flDispatchWeight = tv_dispatchweight.GetFloat();
|
|
if ( flDispatchWeight <= 1.01 )
|
|
flDispatchWeight = 1.01;
|
|
float myRatio = ((float)numClients/(float)numSlots) * flDispatchWeight;
|
|
|
|
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
|
|
}
|
|
|
|
CFmtStr fmtAdditionalInfo;
|
|
const char *pszRelayAddr = pBestProxy->GetUserSetting( "hltv_addr" );
|
|
if ( !pszRelayAddr )
|
|
return false;
|
|
|
|
// If the client is attempting a connection over SDR and the relay downstream allows
|
|
// connection over SDR, then redirect to SDR port instead
|
|
switch ( adr.GetAddressType() )
|
|
{
|
|
case NSAT_PROXIED_CLIENT:
|
|
if ( const char *pszRelaySdrAddr = pBestProxy->GetUserSetting("hltv_sdr") )
|
|
{
|
|
if ( *pszRelaySdrAddr )
|
|
{
|
|
ns_address nsadrsdr;
|
|
if ( nsadrsdr.SetFromString( pszRelaySdrAddr ) && ( nsadrsdr.GetAddressType() == NSAT_PROXIED_GAMESERVER ) )
|
|
{
|
|
// Ensure that client gets a ticket for the new SDR address
|
|
// and that the game server allows redirect
|
|
if ( serverGameDLL->IsValveDS() && serverGameDLL->OnEngineClientProxiedRedirect(
|
|
adr.m_steamID.GetSteamID().ConvertToUint64(), pszRelaySdrAddr, pszRelayAddr ) )
|
|
{
|
|
//
|
|
// Build a P2P HLTV channel SDR address for client redirect
|
|
//
|
|
fmtAdditionalInfo.AppendFormat( " @ %s SDR:%d", pszRelayAddr, nsadrsdr.m_steamID.GetSteamChannel() );
|
|
nsadrsdr.m_steamID.SetSteamChannel( STEAM_P2P_HLTV );
|
|
ns_address_render nsadrRendered( nsadrsdr );
|
|
char *pchStackCopy = ( char * ) stackalloc( 1 + V_strlen( nsadrRendered.String() ) );
|
|
V_strcpy( pchStackCopy, nsadrRendered.String() );
|
|
pszRelayAddr = pchStackCopy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
ConMsg( "Redirecting spectator connect packet from %s to GOTV relay %s%s\n",
|
|
ns_address_render( adr ).String(),
|
|
pszRelayAddr, fmtAdditionalInfo.Access() );
|
|
|
|
// tell the client to connect to this new address
|
|
pNetAdrRedirect->SetFromString( pszRelayAddr );
|
|
|
|
// 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;
|
|
}
|
|
|
|
CON_COMMAND( tv_status, "Show GOTV server status." )
|
|
{
|
|
int slots, proxies, clients;
|
|
float in, out;
|
|
char gd[MAX_OSPATH];
|
|
|
|
Q_FileBase( com_gamedir, gd, sizeof( gd ) );
|
|
|
|
for ( CActiveHltvServerSelector hltv( args ); hltv; hltv.Next() )
|
|
{
|
|
hltv->GetNetStats( in, out );
|
|
|
|
in /= 1024; // as KB
|
|
out /= 1024;
|
|
|
|
ConMsg( "--- GOTV[%u] Status ---\n", hltv.GetIndex() );
|
|
ConMsg( "Online %s, FPS %.1f, Version %i (%s)\n",
|
|
COM_FormatSeconds( hltv->GetOnlineTime() ), hltv->m_flFPS, build_number(),
|
|
#if defined( _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, rate %.1f\n", hltv->GetName(), hltv->GetDirector()->GetDelay(), hltv->GetSnapshotRate() );
|
|
}
|
|
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 );
|
|
|
|
hltv->GetExternalStats( slots, clients );
|
|
if ( slots > 0 )
|
|
{
|
|
if ( clients > 0 )
|
|
ConMsg( "Streaming spectators %i, linked to Steam %i\n", slots, clients );
|
|
else
|
|
ConMsg( "Streaming spectators %i\n", slots );
|
|
}
|
|
|
|
if ( hltv->m_DemoRecorder.IsRecording() )
|
|
{
|
|
ConMsg( "Recording to \"%s\", length %s.\n", hltv->m_DemoRecorder.GetDemoFilename(),
|
|
COM_FormatSeconds( host_state.interval_per_tick * hltv->m_DemoRecorder.GetRecordingTick() ) );
|
|
}
|
|
|
|
if ( hltv->m_Broadcast.IsRecording() )
|
|
{
|
|
ConMsg( "Broadcasting\n" );
|
|
}
|
|
|
|
ConMsg( "\n" );
|
|
|
|
extern ConVar host_name;
|
|
ConMsg( "hostname: %s\n", host_name.GetString() );
|
|
// the header for the status rows
|
|
ConMsg( "# userid name uniqueid connected ping loss state rate adr\n" );
|
|
for ( int j = 0; j < hltv->GetClientCount(); j++ )
|
|
{
|
|
IClient *client = hltv->GetClient( j );
|
|
if ( !client || !client->IsConnected() )
|
|
continue; // not connected yet, maybe challenging
|
|
|
|
extern void Host_Status_PrintClient( IClient *client, bool bShowAddress, void( *print ) ( const char *fmt, ... ) );
|
|
Host_Status_PrintClient( client, true, ConMsg );
|
|
}
|
|
}
|
|
ConMsg( "#end\n" );
|
|
}
|
|
|
|
CON_COMMAND( sv_getinfo, "Show user info of a connected client" )
|
|
{
|
|
if ( args.ArgC() < 4 )
|
|
{
|
|
ConMsg( "Usage: userinfo_show [sv|tv0|tv1] [id] [var]\n" );
|
|
return;
|
|
}
|
|
|
|
CBaseServer *psv = &sv;
|
|
if ( char const *szTvN = StringAfterPrefix( args.Arg( 1 ), "tv" ) )
|
|
{
|
|
int nTV = V_atoi( szTvN );
|
|
nTV = clamp( nTV, 0, HLTV_SERVER_MAX_COUNT - 1 );
|
|
psv = g_pHltvServer[ nTV ];
|
|
if ( !psv )
|
|
{
|
|
ConMsg( "TV%d not active\n", nTV );
|
|
return;
|
|
}
|
|
}
|
|
else if ( !psv )
|
|
{
|
|
ConMsg( "Main server not active\n" );
|
|
return;
|
|
}
|
|
|
|
int nClientID = V_atoi( args.Arg( 2 ) );
|
|
if ( nClientID < 0 || nClientID >= psv->GetClientCount() )
|
|
{
|
|
ConMsg( "Found %d clients on server\n", psv->GetClientCount() );
|
|
return;
|
|
}
|
|
|
|
IClient *pClient = psv->GetClient( nClientID );
|
|
if ( !pClient || !pClient->IsConnected() )
|
|
{
|
|
ConMsg( "Client #%d is %s\n", nClientID, pClient ? "not connected" : "null" );
|
|
return;
|
|
}
|
|
|
|
const char *pszVar = args.Arg( 3 );
|
|
const char *pszValue = pClient->GetUserSetting( pszVar );
|
|
ConMsg( "Client #%d '%s'<%s> '%s'='%s'\n", nClientID, pClient->GetClientName(), pClient->GetNetworkIDString(), pszVar, pszValue );
|
|
}
|
|
|
|
CON_COMMAND( tv_relay, "Connect to GOTV server and relay broadcast." )
|
|
{
|
|
if ( args.ArgC() < 2 )
|
|
{
|
|
ConMsg( "Usage: tv_relay <ip:port> [-instance <inst>]\n" );
|
|
return;
|
|
}
|
|
|
|
const char *address = args.ArgS();
|
|
|
|
// If it's not a single player connection to "localhost", initialize networking & stop listenserver
|
|
if ( StringHasPrefixCaseSensitive( address, "localhost" ) )
|
|
{
|
|
ConMsg( "GOTV can't connect to localhost.\n" );
|
|
return;
|
|
}
|
|
|
|
int nHltvIndex = clamp( args.FindArgInt( "-instance", 0 ), 0, HLTV_SERVER_MAX_COUNT - 1 );
|
|
CHLTVServer * & hltv = g_pHltvServer[ nHltvIndex ];
|
|
|
|
if ( !hltv )
|
|
{
|
|
hltv = new CHLTVServer( nHltvIndex, ( nHltvIndex ? tv_snapshotrate1.GetFloat() : tv_snapshotrate.GetFloat() ) );
|
|
hltv->Init( NET_IsDedicated() );
|
|
}
|
|
|
|
if ( hltv->m_bMasterOnlyMode )
|
|
{
|
|
ConMsg("GOTV[%d] in Master-Only mode.\n", nHltvIndex );
|
|
return;
|
|
}
|
|
|
|
// If the main server instance is running then we want to re-use it's
|
|
// logged on anonymous SteamID
|
|
if ( sv.IsDedicated() && sv.IsActive() )
|
|
sv.FlagForSteamIDReuseAfterShutdown();
|
|
|
|
// shutdown anything else
|
|
Host_Disconnect( false );
|
|
|
|
// start networking
|
|
NET_Init( NET_IsDedicated() );
|
|
NET_SetMultiplayer( true );
|
|
|
|
hltv->ConnectRelay( address );
|
|
}
|
|
|
|
CON_COMMAND( tv_stop, "Stops the GOTV broadcast [-instance <inst> ]" )
|
|
{
|
|
for ( CActiveHltvServerSelector hltv( args ); hltv; hltv.Next() )
|
|
{
|
|
int nClients = hltv->GetNumClients();
|
|
|
|
hltv->Shutdown();
|
|
|
|
ConMsg( "GOTV[%u] stopped, %i clients disconnected.\n", hltv.GetIndex(), nClients );
|
|
}
|
|
}
|
|
|
|
CON_COMMAND( tv_retry, "Reconnects the GOTV relay proxy " )
|
|
{
|
|
for ( CActiveHltvServerSelector hltv( args ); hltv; hltv.Next() )
|
|
{
|
|
if ( hltv->m_bMasterOnlyMode )
|
|
{
|
|
ConMsg( "GOTV[%u] in Master-Only mode.\n", hltv.GetIndex() );
|
|
return;
|
|
}
|
|
|
|
if ( !hltv->m_ClientState.m_Remote.Count() )
|
|
{
|
|
ConMsg( "Can't retry, no previous GOTV[%u] connection\n", hltv.GetIndex() );
|
|
return;
|
|
}
|
|
|
|
ConMsg( "Commencing GOTV[%u] connection retry to %s\n", hltv.GetIndex(), hltv->m_ClientState.m_Remote.Get( 0 ).m_szRetryAddress.String() );
|
|
Cbuf_AddText( Cbuf_GetCurrentPlayer(), va( "tv_relay %s\n", hltv->m_ClientState.m_Remote.Get( 0 ).m_szRetryAddress.String() ) );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
CON_COMMAND( tv_record, "Starts GOTV demo recording [-instance <inst> ]" )
|
|
{
|
|
if ( args.ArgC() < 2 )
|
|
{
|
|
ConMsg( "Usage: tv_record <filename> [-instance <inst> ]\n" );
|
|
return;
|
|
}
|
|
|
|
int nHltvInstance = clamp( args.FindArgInt( "-instance", 0 ), 0, HLTV_SERVER_MAX_COUNT );
|
|
CHLTVServer * hltv = g_pHltvServer[ nHltvInstance ];
|
|
if ( hltv && hltv->IsActive() )
|
|
{
|
|
if ( !hltv->IsMasterProxy() )
|
|
{
|
|
ConMsg( "GOTV[%u]: Only GOTV Master can record demos instantly.\n", nHltvInstance );
|
|
return;
|
|
}
|
|
|
|
if ( hltv->m_DemoRecorder.IsRecording() )
|
|
{
|
|
ConMsg( "GOTV[%u] already recording to %s.\n", nHltvInstance, hltv->m_DemoRecorder.GetDemoFilename() );
|
|
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 ) );
|
|
|
|
bool bConflict = false;
|
|
for ( CHltvServerIterator other; other; other.Next() )
|
|
{
|
|
CHLTVServer *pOtherHltvServer = other;
|
|
if ( pOtherHltvServer != hltv && pOtherHltvServer->IsRecording() && !V_stricmp( pOtherHltvServer->GetRecordingDemoFilename(), name ) )
|
|
{
|
|
Warning( "Cannot record on GOTV[%d]: another GOTV[%d] is currently recording into that file\n", nHltvInstance, pOtherHltvServer->GetInstanceIndex() );
|
|
bConflict = true;
|
|
}
|
|
}
|
|
|
|
if ( !bConflict )
|
|
{
|
|
hltv->m_DemoRecorder.StartRecording( name, false );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ConMsg( "GOTV[%d] is not active\n", nHltvInstance );
|
|
}
|
|
}
|
|
|
|
|
|
// tv_broadcast change callback
|
|
void OnTvBroadcast( )
|
|
{
|
|
for ( CActiveHltvServerIterator hltv; hltv; hltv.Next() )
|
|
{
|
|
if ( GetIndexedConVar( tv_broadcast, hltv.GetIndex() ).GetBool() )
|
|
{
|
|
if ( hltv->IsTVRelay() )
|
|
{
|
|
Warning( "GOTV[%d] is a relay.", hltv.GetIndex() );
|
|
}
|
|
else
|
|
{
|
|
if ( !hltv->m_Broadcast.IsRecording() )
|
|
{
|
|
hltv->StartBroadcast();
|
|
ConMsg( "Broadcast on GOTV[%d] started\n", hltv.GetIndex() );
|
|
}
|
|
else
|
|
{
|
|
ConMsg( "Broadcast on GOTV[%d] is already active\n", hltv.GetIndex() );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( hltv->m_Broadcast.IsRecording() )
|
|
{
|
|
hltv->m_Broadcast.StopRecording();
|
|
ConMsg( "Broadcast on GOTV[%d] stopped\n", hltv.GetIndex() );
|
|
}
|
|
else
|
|
{
|
|
ConMsg( "Broadcast on GOTV[%d] is not active\n", hltv.GetIndex() );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
void OnTvBroadcast( IConVar *var, const char *pOldValue, float flOldValue ) { OnTvBroadcast(); }
|
|
|
|
|
|
CON_COMMAND( tv_broadcast_status, "Print out broadcast status" )
|
|
{
|
|
int nActiveServers = 0, nBroadcastingServers = 0;
|
|
for ( CActiveHltvServerIterator hltv; hltv; hltv.Next() )
|
|
{
|
|
nActiveServers++;
|
|
if ( hltv->m_Broadcast.IsRecording() )
|
|
{
|
|
nBroadcastingServers++;
|
|
Msg( "GOTV[%d] is broadcasting to %s: ", hltv.GetIndex(), hltv->m_Broadcast.GetUrl() );
|
|
hltv->m_Broadcast.DumpStats();
|
|
}
|
|
}
|
|
if ( !nBroadcastingServers )
|
|
{
|
|
// print something
|
|
if ( nActiveServers )
|
|
Msg( "GOTV is not broadcasting\n" );
|
|
else
|
|
Msg( "GOTV is not active\n" );
|
|
}
|
|
}
|
|
|
|
// tv_stopbroadcast is effectively accomplished by tv_broadcast 0
|
|
|
|
CON_COMMAND( tv_stoprecord, "Stops GOTV demo recording [-instance <inst> ]" )
|
|
{
|
|
for ( CActiveHltvServerSelector hltv( args ); hltv; hltv.Next() )
|
|
{
|
|
//this is painful, but we need to expand all of the delta frames before we stop recording. That means this console command will cause a big spike in memory
|
|
hltv->StopRecording();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
CON_COMMAND( tv_clients, "Shows list of connected GOTV clients [-instance <inst> ]" )
|
|
{
|
|
for ( CActiveHltvServerSelector hltv( args ); hltv; hltv.Next() )
|
|
{
|
|
int nCount = 0;
|
|
ConMsg( "GOTV[%u]\n", hltv.GetIndex() );
|
|
|
|
for ( int i = 0; i < hltv->GetClientCount(); i++ )
|
|
{
|
|
CHLTVClient *client = hltv->Client( i );
|
|
INetChannel *netchan = client->GetNetChannel();
|
|
|
|
if ( !netchan )
|
|
continue;
|
|
|
|
bool bClientIsHLTV = client->IsHLTV();
|
|
char const *szClientHLTVRedirect = bClientIsHLTV ? client->GetUserSetting( "hltv_addr" ) : NULL;
|
|
|
|
ConMsg( "ID: %i, \"%s\"%s, Time %s, %s%s%s, In %.1f, Out %.1f.\n",
|
|
client->GetUserID(),
|
|
client->GetClientName(),
|
|
bClientIsHLTV ? " (Relay)" : "",
|
|
COM_FormatSeconds( netchan->GetTimeConnected() ),
|
|
netchan->GetAddress(),
|
|
bClientIsHLTV ? ( szClientHLTVRedirect ? " redirecting to " : " BAD REDIRECT ADDR" ) : "",
|
|
( bClientIsHLTV && szClientHLTVRedirect ) ? szClientHLTVRedirect : "",
|
|
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 [-instance <inst> ]" )
|
|
{
|
|
for ( CActiveHltvServerSelector hltv( args ); hltv; hltv.Next() )
|
|
{
|
|
IGameEvent *msg = g_GameEventManager.CreateEvent( "hltv_message", true );
|
|
|
|
if ( msg )
|
|
{
|
|
msg->SetString( "text", args.ArgS() );
|
|
hltv->BroadcastEventLocal( msg, false );
|
|
g_GameEventManager.FreeEvent( msg );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
CActiveHltvServerSelector::CActiveHltvServerSelector( const CCommand &args )
|
|
{
|
|
const char *pInstance = args.FindArg( "-instance" );
|
|
m_nIndex = HLTV_SERVER_MAX_COUNT; // by default, iterator is empty/invalid
|
|
m_nMask = 0;
|
|
|
|
if ( ( pInstance && !V_stricmp( pInstance, "all" ) ) || args.FindArg( "-all" ) )
|
|
{
|
|
// iterate all active instances
|
|
m_nMask = ( 1 << HLTV_SERVER_MAX_COUNT ) - 1;
|
|
m_nIndex = -1;
|
|
Next();
|
|
if ( m_nIndex >= HLTV_SERVER_MAX_COUNT )
|
|
{
|
|
ConMsg( "No active GOTV instances at this time.\n" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int nInstance = pInstance ? V_atoi( pInstance ) : 0; // the default is GOTV[0]
|
|
if ( nInstance >= HLTV_SERVER_MAX_COUNT )
|
|
{
|
|
ConMsg( "GOTV[%d] index out of range.\n", nInstance );
|
|
}
|
|
else if ( g_pHltvServer[ nInstance ] && g_pHltvServer[ nInstance ]->IsActive() )
|
|
{
|
|
m_nMask = 1 << nInstance;
|
|
m_nIndex = nInstance; // valid GOTV instance found
|
|
}
|
|
else
|
|
{
|
|
ConMsg( "GOTV[%d] not active.\n", nInstance );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CON_COMMAND( tv_mem, "hltv memory statistics" )
|
|
{
|
|
bool bActive = false;
|
|
for ( CActiveHltvServerIterator hltv; hltv; hltv.Next() )
|
|
{
|
|
hltv->DumpMem();
|
|
bActive = true;
|
|
}
|
|
if ( !bActive )
|
|
Msg( "No active GOTV servers found\n" );
|
|
}
|
|
|
|
void CHLTVServer::DumpMem()
|
|
{
|
|
Msg( "GOTV[%d] memory consumption:", m_nInstanceIndex );
|
|
uint nHltvFrameSize = 0, nHltvFrameCount = 0;
|
|
for ( CHLTVFrame *pFrame = m_CurrentFrame; pFrame; pFrame = static_cast< CHLTVFrame * > ( pFrame->m_pNext ) )
|
|
{
|
|
nHltvFrameSize += pFrame->GetMemSize();
|
|
nHltvFrameCount++;
|
|
}
|
|
Msg( "%4u Hltv Frames: %10s\n", nHltvFrameCount, V_pretifynum( nHltvFrameSize ) );
|
|
|
|
uint nDeltaFrameCount = 0, nDeltaFrameSize = 0;
|
|
for ( SHLTVDeltaFrame_t *pFrame = m_pOldestDeltaFrame; pFrame; pFrame = pFrame->m_pNewerDeltaFrame )
|
|
{
|
|
nDeltaFrameSize += pFrame->GetMemSize();
|
|
nDeltaFrameCount++;
|
|
}
|
|
Msg( "%4u Delta Frames: %10s\n", nDeltaFrameCount, V_pretifynum( nDeltaFrameSize ) );
|
|
// dump packed entity sizes from framesnapshotmanager?
|
|
}
|
|
|
|
#ifndef DEDICATED
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void EditDemo_f( const CCommand &args )
|
|
{
|
|
if ( args.ArgC() < 2 )
|
|
{
|
|
Msg ("editdemo <demoname> [-instance <inst>]: edits a demo\n");
|
|
return;
|
|
}
|
|
|
|
CActiveHltvServerSelector hltv( args );
|
|
if ( hltv )
|
|
{
|
|
// 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;
|
|
|
|
hltv->StartPlayback( name, false, NULL, -1 );
|
|
}
|
|
}
|
|
|
|
CON_COMMAND_AUTOCOMPLETEFILE( editdemo, EditDemo_f, "Edit a recorded demo file (.dem ).", NULL, dem );
|
|
#endif
|