Counter Strike : Global Offensive Source Code
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.
 
 
 
 
 
 

742 lines
20 KiB

//========= Copyright (c) Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $NoKeywords: $
//=============================================================================//
#include "client_pch.h"
#include "cl_demo.h"
#include "cl_broadcast.h"
#include "baseautocompletefilelist.h"
#include "demostreamhttp.h"
#include "dt_common_eng.h"
#include "matchmaking/imatchframework.h"
extern ConVar demo_debug;
extern CNetworkStringTableContainer *networkStringTableContainerClient;
extern bool IsControlCommand( unsigned char cmd );
extern ConVar tv_playcast_delay_prediction;
extern ConVar tv_playcast_max_rcvage;
CBroadcastPlayer s_ClientBroadcastPlayer;
CBroadcastPlayer::CBroadcastPlayer() :
m_DemoStrider( NULL )
{
m_bPlayingBack = false;
m_flPlaybackRateModifier = 1.0f;
m_nSkipToTick = -1;
m_flAutoResumeTime = 0.0f;
m_bPlaybackPaused = false;
m_nPacketTick = 0;
m_nStartHostTick = 0;
m_nStreamStartTick = 0;
m_bInterpolateView = false;
m_DemoStream.SetClient( this );
m_nStreamState = STREAM_STOP;
m_bPacketReadSuspended = false;
m_dResyncTimerStart = 0;
m_bIgnoreDemoStopCommand = false;
}
CBroadcastPlayer::~CBroadcastPlayer()
{
if ( m_bPlayingBack )
{
StopPlayback();
if ( g_ClientDLL )
{
g_ClientDLL->OnDemoPlaybackStop();
}
}
}
IDemoStream * CBroadcastPlayer::GetDemoStream()
{
return &m_DemoStream;
}
void CBroadcastPlayer::StartStreaming( const char *url, const char *options )
{
if ( !options )
options = "";
m_bSkipSync = ( NULL != V_strstr( options, "c" ) ); // playback akamai cached content (sync isn't cached)
m_bIgnoreDemoStopCommand = ( NULL != V_strstr( options, "b" ) );
int nPlayFromFragment = 0;
if ( const char *pFrame = V_strstr( options, "f" ) )
nPlayFromFragment = V_atoi( pFrame + 1 );
else if ( NULL != V_strstr( options, "a" ) )
nPlayFromFragment = 1;
StopPlayback();
if ( g_pClientDemoPlayer )
g_pClientDemoPlayer->StopPlayback();
demoplayer = this;
if ( CDemoPlaybackParameters_t *pParams = const_cast< CDemoPlaybackParameters_t * >( GetDemoPlaybackParameters() ) )
{
// the format of the id is MatchId# where # is a one-digit GoTv instance index
pParams->m_uiLiveMatchID = CDemoStreamHttp::GetStreamId( url ).m_nMatchId;
}
if ( StartStreamingInternal() )
{
g_ClientDLL->OnDemoPlaybackStart( url );
if ( m_bSkipSync )
{
int nGuessedStartFragment = Max( 1, nPlayFromFragment );
m_DemoStream.StartStreamingCached( url, nGuessedStartFragment );
}
else
{
if ( nPlayFromFragment > 0 )
m_DemoStream.StartStreaming( url, CDemoStreamHttp::SyncParams_t( nPlayFromFragment ) );
else
m_DemoStream.StartStreaming( url );
}
}
}
bool CBroadcastPlayer::OnEngineGotvSyncPacket( const CEngineGotvSyncPacket *pPkt )
{
return m_DemoStream.OnEngineGotvSyncPacket( pPkt );
}
bool CBroadcastPlayer::StartStreamingInternal()
{
SCR_BeginLoadingPlaque();
// Disconnect from server or stop running one
int oldn = GetBaseLocalClient().demonum;
GetBaseLocalClient().demonum = -1;
Host_Disconnect( false );
// set current demo player to client demo player
// disconnect before loading demo, to avoid sometimes loading into game instead of demo
GetBaseLocalClient().Disconnect( false );
GetBaseLocalClient().demonum = oldn;
GetBaseLocalClient().m_nSignonState = SIGNONSTATE_CONNECTED;
ResyncDemoClock();
// create a fake channel with a NULL address (no encryption keys in demos)
GetBaseLocalClient().m_NetChannel = NET_CreateNetChannel( NS_CLIENT, NULL, "BROADCAST", &GetBaseLocalClient(), NULL, false );
if ( !GetBaseLocalClient().m_NetChannel )
{
ConMsg( "Broadcast Player: failed to create demo net channel\n" );
m_DemoStream.StopStreaming();
GetBaseLocalClient().demonum = -1; // stop demo loop
Host_Disconnect( true );
SCR_EndLoadingPlaque();
return false;
}
GetBaseLocalClient().m_NetChannel->SetTimeout( -1.0f ); // never timeout
m_bPlayingBack = true;
m_bPlaybackPaused = false;
m_flAutoResumeTime = 0.0f;
m_flPlaybackRateModifier = 1.0f;
m_bInterpolateView = false;
m_nStartHostTick = -1;
m_nStreamFragment = -1;
m_nStreamStartTick = -1;
V_memset( &m_DemoPacket, 0, sizeof( m_DemoPacket ) );
// setup demo packet data buffer
m_DemoPacket.data = NULL;
m_DemoPacket.from.SetAddrType( NSAT_NETADR );
m_DemoPacket.from.m_adr.SetType( NA_LOOPBACK );
m_DemoStrider.Set( NULL );
GetBaseLocalClient().chokedcommands = 0;
GetBaseLocalClient().lastoutgoingcommand = -1;
GetBaseLocalClient().m_flNextCmdTime = net_time;
m_dResyncTimerStart = Plat_FloatTime();
m_nStreamState = STREAM_SYNC;
return true;
}
bool CBroadcastPlayer::OnDemoStreamRestarting()
{
return StartStreamingInternal();
}
void CBroadcastPlayer::ResyncStream()
{
if ( Plat_FloatTime() - m_dResyncTimerStart > m_DemoStream.GetBroadcastKeyframeInterval() )
{
m_dResyncTimerStart = Plat_FloatTime();
DevMsg( "Stream Re-sync @%d...\n", m_nStreamFragment );
m_nStreamFragment++; // resync from the next fragment
m_nStreamState = STREAM_SYNC;
m_DemoStream.Resync( );
}
}
void CBroadcastPlayer::OnDemoStreamStart( const DemoStreamReference_t &start, int nResync )
{
m_dResyncTimerStart = Plat_FloatTime();
m_nStreamState = nResync ? STREAM_FULLFRAME : STREAM_START;
m_nStartHostTick = host_tickcount;
m_nStreamStartTick = start.nTick;
m_nStreamFragment = start.nFragment;
}
void CBroadcastPlayer::OnDemoStreamStop()
{
if ( m_bPlayingBack )
{
m_bPlayingBack = false;
}
m_flAutoResumeTime = 0.0f;
m_flPlaybackRateModifier = 1.0f;
m_DemoPacket.data = NULL;
m_DemoStrider.Set( NULL );
m_nStreamState = STREAM_STOP;
if ( g_pMatchFramework )
{
g_pMatchFramework->GetEventsSubscription()->BroadcastEvent( new KeyValues( "OnDemoFileEndReached" ) );
}
FOR_EACH_VALID_SPLITSCREEN_PLAYER( hh )
{
ACTIVE_SPLITSCREEN_PLAYER_GUARD( hh );
GetBaseLocalClient().Disconnect( true );
}
demoplayer = g_pClientDemoPlayer;
g_ClientDLL->OnDemoPlaybackStop();
}
CON_COMMAND( playcast, "Play a broadcast" )
{
if ( args.ArgC() < 2 )
{
ConMsg( "Usage: playcast <url>\n" );
return;
}
if ( !net_time && !NET_IsMultiplayer() )
{
ConMsg( "Deferring playcast command!\n" );
return;
}
s_ClientBroadcastPlayer.StartStreaming( args[ 1 ], args.ArgC() >= 3 ? args[2] : "" );
}
void CBroadcastPlayer::PausePlayback( float seconds )
{
m_bPlaybackPaused = true;
if ( seconds > 0.0f )
{
// Use true clock since everything else is frozen
m_flAutoResumeTime = Sys_FloatTime() + seconds;
}
else
{
m_flAutoResumeTime = 0.0f;
}
}
void CBroadcastPlayer::ResumePlayback()
{
m_bPlaybackPaused = false;
m_flAutoResumeTime = 0.0f;
}
void CBroadcastPlayer::StopPlayback()
{
m_nStreamState = STREAM_STOP;
m_DemoStream.StopStreaming();
g_ClientDLL->OnDemoPlaybackStop();
}
bool CBroadcastPlayer::PreparePacket( void )
{
Assert( !m_DemoStrider.Get() || ( m_DemoStrider.Get() >= m_DemoBuffer->Base() && m_DemoStrider.Get() <= m_DemoBuffer->End() ) );
double dTime = Plat_FloatTime();
if ( !m_DemoStrider.Get() || m_DemoStrider.Get() >= m_DemoBuffer->End() )
{
// get the next packet
switch ( m_nStreamState )
{
case STREAM_START:
if ( CDemoStreamHttp::Buffer_t *pBuffer = m_DemoStream.GetStreamSignupBuffer() )
{
SetDemoBuffer( pBuffer );
m_nStreamState = STREAM_FULLFRAME;
if ( tv_playcast_delay_prediction.GetBool() )
{
m_nStreamState = STREAM_MAP_LOADED;
}
}
else
return false; // not implemented yet: the signup buffer isn't precached
break;
case STREAM_MAP_LOADED:
{// recompute the start frame
DemoStreamReference_t startRef = m_DemoStream.GetStreamStartReference( true );
if ( demo_debug.GetBool() )
Msg( "playcast: Adjusting fragment %d->%d, tick %d->%d\n", m_nStreamFragment, startRef.nFragment, m_nStreamStartTick, startRef.nTick );
m_nStartHostTick = host_tickcount - startRef.nSkipTicks;
m_nStreamStartTick = startRef.nTick;
m_nStreamFragment = startRef.nFragment;
m_DemoStream.BeginBuffering( m_nStreamFragment );
m_nStreamState = STREAM_WAITING_FOR_KEYFRAME;
m_dResyncTimerStart = m_dDelayedPrecacheTimeStart = dTime;
return false; // the data isn't ready yet
}
break;
case STREAM_WAITING_FOR_KEYFRAME:
if ( m_dDelayedPrecacheTimeStart + tv_playcast_max_rcvage.GetFloat() < dTime )
{
ResyncStream(); // try to resync when the resync timeout passes; maybe the full fragment is about to come in
return false;
}
if ( CDemoStreamHttp::Buffer_t *pBuffer = m_DemoStream.GetFragmentBuffer( m_nStreamFragment, CDemoStreamHttp::FRAGMENT_FULL ) )
{
// is the delta precached?
if ( m_DemoStream.GetFragmentBuffer( m_nStreamFragment, CDemoStreamHttp::FRAGMENT_DELTA ) )
{// the data is ready
if ( demo_debug.GetBool() )
Msg( "playcast: Fragment %d keyframe and delta frames ready, delayed precache took %.2f sec\n", m_nStreamFragment, dTime - m_dDelayedPrecacheTimeStart );
SetDemoBuffer( pBuffer );
m_nStreamState = STREAM_FULLFRAME;
return true;
}
}
return false;
case STREAM_FULLFRAME:
m_DemoStream.ReleaseFragment( m_nStreamFragment - 1 );
if ( CDemoStreamHttp::Buffer_t *pBuffer = m_DemoStream.GetFragmentBuffer( m_nStreamFragment, CDemoStreamHttp::FRAGMENT_FULL ) )
{
if ( demo_debug.GetBool() )
Msg( "playcast: Play keyframe Fragment %d\n", m_nStreamFragment );
SetDemoBuffer( pBuffer );
m_nStreamState = STREAM_BEFORE_DELTAFRAMES;
}
else
{
ResyncStream();
return false;
}
break;
case STREAM_BEFORE_DELTAFRAMES:
if ( g_ClientDLL )
g_ClientDLL->OnDemoPlaybackTimeJump();
m_nStreamState = STREAM_DELTAFRAMES;
// fall through and start streaming deltaframes now
case STREAM_DELTAFRAMES:
m_DemoStream.ReleaseFragment( m_nStreamFragment - 1 );
if ( CDemoStreamHttp::Buffer_t *pBuffer = m_DemoStream.GetFragmentBuffer( m_nStreamFragment, CDemoStreamHttp::FRAGMENT_DELTA ) )
{
if ( demo_debug.GetBool() )
Msg( "playcast: Play delta Fragment %d\n", m_nStreamFragment );
SetDemoBuffer( pBuffer );
m_nStreamState = STREAM_DELTAFRAMES;
m_nStreamFragment++; // TODO : count the ticks, not fragments
for ( int i = 1; i <= 4; ++i )
m_DemoStream.RequestFragment( m_nStreamFragment + i, CDemoStreamHttp::FRAGMENT_DELTA );
}
else
{
// we don't have the delta... we can request the full fragment and restart, or we could do a partial re-sync (we don't need to reload the level, hence the partial)
ResyncStream();
return false;
}
break;
}
}
m_dResyncTimerStart = dTime;
return true;
}
void CBroadcastPlayer::ReadCmdHeader( unsigned char& cmd, int& tick, int &nPlayerSlot )
{
if ( !m_DemoStrider.Get() || m_DemoStrider.Get() + 6 > m_DemoBuffer->End() )
{
cmd = dem_stop;
tick = m_nPacketTick;
nPlayerSlot = 0;
}
else
{
cmd = m_DemoStrider.StrideUnaligned<uint8>();
tick = m_DemoStrider.StrideUnaligned<int32>();
nPlayerSlot = m_DemoStrider.StrideUnaligned<uint8>();
if ( demo_debug.GetInt() > 2 )
{
if ( cmd == dem_packet )
DevMsg( "playcast: Packet tick %d size %u\n", tick, *( uint32* )m_DemoStrider.Get() );
}
}
}
//-----------------------------------------------------------------------------
// Purpose: Read in next demo message and send to local client over network channel, if it's time.
// Output : bool
//-----------------------------------------------------------------------------
netpacket_t *CBroadcastPlayer::ReadPacket( void )
{
int tick = 0;
byte cmd = dem_signon;
uint8* curpos = NULL;
m_DemoStream.Update();
if ( m_DemoStream.IsIdle() )
{
m_bPlayingBack = false;
Host_EndGame( true, "Tried to read a demo message with no demo file\n" );
return NULL;
}
// dropped: timedemo
// If game is still shutting down, then don't read any demo messages from file quite yet
if ( HostState_IsGameShuttingDown() )
{
return NULL;
}
Assert( IsPlayingBack() );
// External editor has paused playback
if ( CheckPausedPlayback() )
return NULL;
if ( m_nStreamState <= STREAM_SYNC )
return NULL; // waiting for data
// dropped: highlights
bool bStopReading = false;
while ( !bStopReading )
{
if ( !PreparePacket() )
return NULL; // packet is not ready
curpos = m_DemoStrider.Get();
int nPlayerSlot = 0;
ReadCmdHeader( cmd, tick, nPlayerSlot );
tick -= m_nStreamStartTick;
m_nPacketTick = tick;
// always read control commands
if ( !IsControlCommand( cmd ) )
{
int playbacktick = GetPlaybackTick();
if ( GetBaseLocalClient().IsActive() &&
( tick > playbacktick ) && !IsSkipping() )
{
// is not time yet
bStopReading = true;
}
if ( bStopReading )
{
demoaction->Update( false, playbacktick, TICKS_TO_TIME( playbacktick ) );
m_DemoStrider.Set( curpos ); // go back to start of current demo command
return NULL; // Not time yet, dont return packet data.
}
}
// COMMAND HANDLERS
switch ( cmd )
{
case dem_synctick: // currently not used, but may need to rethink it in the future
ResyncDemoClock();
break;
case dem_stop:
if ( !m_bIgnoreDemoStopCommand )
{
if ( demo_debug.GetBool() )
{
Msg( "playcast: %d dem_stop\n", tick );
}
if ( g_pMatchFramework )
{
g_pMatchFramework->GetEventsSubscription()->BroadcastEvent( new KeyValues( "OnDemoFileEndReached" ) );
}
FOR_EACH_VALID_SPLITSCREEN_PLAYER( hh )
{
ACTIVE_SPLITSCREEN_PLAYER_GUARD( hh );
GetBaseLocalClient().Disconnect( true );
}
return NULL;
}
break;
case dem_consolecmd: // currently not used, not tested
{
ACTIVE_SPLITSCREEN_PLAYER_GUARD( nPlayerSlot );
if ( const char *command = m_DemoStrider.StrideString( m_DemoBuffer->End() ) )
{
if ( demo_debug.GetBool() )
{
Msg( "playcast: %d dem_consolecmd [%s]\n", tick, command );
}
Cbuf_AddText( Cbuf_GetCurrentPlayer(), command, kCommandSrcDemoFile );
Cbuf_Execute();
}
}
break;
case dem_datatables:
{
if ( demo_debug.GetBool() )
{
Msg( "playcast: %d dem_datatables\n", tick );
}
// support for older engine demos
bf_read buf( m_DemoStrider.Get(), GetReminingStrideLength() );
if ( !DataTable_LoadDataTablesFromBuffer( &buf, m_DemoStream.GetDemoProtocol() ) )
{
Host_Error( "Error parsing network data tables during demo playback." );
}
m_DemoStrider.Stride<uint8>( buf.GetNumBytesRead() );
}
break;
case dem_stringtables:
{
bf_read buf( m_DemoStrider.Get(), GetReminingStrideLength() );
if ( !networkStringTableContainerClient->ReadStringTables( buf ) )
{
Host_Error( "Error parsing string tables during demo playback." );
}
m_DemoStrider.Stride<uint8>( buf.GetNumBytesRead() );
}
break;
case dem_usercmd:
{
Assert( !"Not used, Not tested - probably doesn't work correctly!" );
ACTIVE_SPLITSCREEN_PLAYER_GUARD( nPlayerSlot );
if ( demo_debug.GetBool() )
{
Msg( "playcast: %d dem_usercmd\n", tick );
}
int nCmdNumber = m_DemoStrider.StrideUnaligned<int32>(), nUserCmdLength = m_DemoStrider.StrideUnaligned<int32>();
if ( m_DemoStrider.Get() + nUserCmdLength > m_DemoBuffer->End() )
{
Warning( "Invalid user cmd length %d\n", nUserCmdLength );
m_DemoStrider.Set( m_DemoBuffer->End() ); // invalid user cmd length
}
else
{
bf_read msg( "ReadUserCmd", m_DemoStrider.Stride<uint8>( nUserCmdLength ), nUserCmdLength );
g_ClientDLL->DecodeUserCmdFromBuffer( nPlayerSlot, msg, nCmdNumber );
// Note, we need to have the current outgoing sequence correct so we can do prediction
// correctly during playback
GetBaseLocalClient().lastoutgoingcommand = nCmdNumber;
}
}
break;
case dem_customdata:
{
int iCallbackIndex = m_DemoStrider.StrideUnaligned<int32>(), nSize = m_DemoStrider.StrideUnaligned<int32>();
m_DemoStrider.Stride<uint8>( nSize );
Warning( "Unable to decode custom demo data %d bytes, callback %d not supported.\n", nSize, iCallbackIndex );
}
break;
default:
{
bStopReading = true;
if ( IsSkipping() )
{
// adjust playback host_tickcount when skipping
m_nStartHostTick = host_tickcount - tick;
}
}
break;
}
}
if ( cmd == dem_packet )
{
// remember last frame we read a dem_packet update
//m_nTimeDemoCurrentFrame = host_framecount;
}
//int inseq, outseqack, outseq = 0;
// we're skipping cmd info in this protocol: see CHLTVBroadcast::WriteMessages, it doesn't write seq
//GetBaseLocalClient().m_NetChannel->SetSequenceData( outseq, inseq, outseqack );
int length = m_DemoStrider.StrideUnaligned< int32 >();
if ( length < 0 || uint( length ) > GetReminingStrideLength() )
{
Warning( "Invalid broadcast packet size %d in fragment buffer size %d\n", length, m_DemoBuffer->m_nSize );
StrideDemoPacket();
return NULL;
}
else
{
StrideDemoPacket( length );
if ( demo_debug.GetInt() > 2 )
{
Msg( "playcast: %d network packet [%d]\n", tick, length );
}
if ( length > 0 )
{
m_DemoPacket.received = realtime;
if ( demo_debug.GetInt() > 2 )
{
Msg( "playcast: Demo message, tick %i, %i bytes\n", GetPlaybackTick(), length );
}
}
// Try and jump ahead one frame
m_bInterpolateView = true; //ParseAheadForInterval( tick, 8 ); TODO: NOT IMPLEMENTED, camera transitions will be jerky
// ConMsg( "Reading message for %i : %f skip %i\n", m_nFrameCount, fElapsedTime, forceskip ? 1 : 0 );
return &m_DemoPacket;
}
}
void CBroadcastPlayer::SetDemoBuffer( CDemoStreamHttp::Buffer_t * pBuffer )
{
m_DemoStrider.Set( pBuffer->Base() );
m_DemoBuffer = pBuffer;
m_DemoPacket.received = realtime;
m_DemoPacket.data = NULL;
m_DemoPacket.size = 0;
m_DemoPacket.message.StartReading( NULL, 0 ); // message must be set up later
}
void CBroadcastPlayer::StrideDemoPacket( int nLength )
{
m_DemoPacket.data = m_DemoStrider.Stride<unsigned char>( nLength );
m_DemoPacket.size = nLength;
m_DemoPacket.message.StartReading( m_DemoPacket.data, m_DemoPacket.size );
}
void CBroadcastPlayer::StrideDemoPacket( )
{
StrideDemoPacket( GetReminingStrideLength() );
}
uint CBroadcastPlayer::GetReminingStrideLength()
{
return m_DemoBuffer->End() - m_DemoStrider.Get();
}
CDemoPlaybackParameters_t Helper_GetBroadcastPlayerDemoPlaybackParameters()
{
CDemoPlaybackParameters_t params = {};
params.m_bPlayingLiveRemoteBroadcast = true;
params.m_numRoundStop = 999;
return params;
}
CDemoPlaybackParameters_t const * CBroadcastPlayer::GetDemoPlaybackParameters()
{
static CDemoPlaybackParameters_t s_params = Helper_GetBroadcastPlayerDemoPlaybackParameters();
return &s_params;
}
void CBroadcastPlayer::SetPacketReadSuspended( bool bSuspendPacketReading )
{
if ( m_bPacketReadSuspended == bSuspendPacketReading )
return; // same state
m_bPacketReadSuspended = bSuspendPacketReading;
if ( !m_bPacketReadSuspended )
ResyncDemoClock(); // Make sure we resync demo clock when we resume packet reading
}
int CBroadcastPlayer::GetPlaybackStartTick( void )
{
return m_nStartHostTick;
}
int CBroadcastPlayer::GetPlaybackTick( void )
{
return host_tickcount - m_nStartHostTick;
}
int CBroadcastPlayer::GetPlaybackDeltaTick( void )
{
return host_tickcount - m_nStartHostTick;
}
int CBroadcastPlayer::GetPacketTick()
{
return m_nPacketTick;
}
void CBroadcastPlayer::ResyncDemoClock()
{
m_nStartHostTick = host_tickcount;
m_nPreviousTick = m_nStartHostTick;
}
bool CBroadcastPlayer::CheckPausedPlayback( void )
{
if ( m_bPacketReadSuspended )
return true; // When packet reading is suspended it trumps all other states
return m_bPlaybackPaused;
}
float CBroadcastPlayer::GetPlaybackTimeScale()
{
return m_flPlaybackRateModifier;
}
void CBroadcastPlayer::SetPlaybackTimeScale( float timescale )
{
m_flPlaybackRateModifier = timescale;
}
bool CBroadcastPlayer::IsPlaybackPaused( void ) const
{
return m_bPlayingBack && m_bPlaybackPaused;
}