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.
539 lines
16 KiB
539 lines
16 KiB
//=========== Copyright Valve Corporation, All rights reserved. ==============//
|
|
|
|
#include "cbase.h"
|
|
#include "fatdemo.h"
|
|
|
|
#include "baseplayer_shared.h"
|
|
#include "cs_gamerules.h"
|
|
#include "gametypes/igametypes.h"
|
|
|
|
#ifdef CLIENT_DLL
|
|
#include "c_team.h"
|
|
#include "c_playerresource.h"
|
|
#include "c_cs_player.h"
|
|
#include "c_cs_playerresource.h"
|
|
#else
|
|
#include "team.h"
|
|
|
|
#include "cs_player.h"
|
|
#include "cs_player_resource.h"
|
|
#endif
|
|
|
|
#include "weapon_csbase.h"
|
|
#include "cs_weapon_parse.h"
|
|
#include "proto_oob.h" // For MAKE_4BYTES
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
#if !defined( CSTRIKE_REL_BUILD )
|
|
|
|
// Globals
|
|
CCSFatDemoRecorder g_fatDemoRecorder;
|
|
CCSFatDemoRecorder *g_pFatDemoRecorder = &g_fatDemoRecorder;
|
|
|
|
ConVar csgo_fatdemo_enable( "csgo_fatdemo_enable", "0", FCVAR_RELEASE );
|
|
ConVar csgo_fatdemo_output( "csgo_fatdemo_output", "test.fatdem", FCVAR_RELEASE );
|
|
|
|
// The file structure is thus:
|
|
// FatDemoHeader
|
|
// for each protobuf message:
|
|
// size of message
|
|
// protobuf message
|
|
|
|
struct FatDemoHeader
|
|
{
|
|
uint32 m_magic; // Must be characters "GOML",
|
|
uint32 m_version; // Which version of the header. Protobuf mechanisms are used for the actual payloads.
|
|
};
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CaptureGameState( MLGameState* pOutState );
|
|
void CaptureMatchState( MLMatchState* pOutState );
|
|
void CaptureRoundState( MLRoundState* pOutState );
|
|
void CapturePlayerState( MLPlayerState* pOutState, CCSPlayer* pCsPlayer );
|
|
void CaptureWeaponState( MLWeaponState* pOutState, CWeaponCSBase* pCsWeapon, int index, CCSPlayer* pCsPlayer );
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------------------------------
|
|
class CCSFatDemoEventVisitor : public IGameEventVisitor2
|
|
{
|
|
public:
|
|
CCSFatDemoEventVisitor( MLEvent* pEvent )
|
|
: m_pEvent( pEvent )
|
|
{}
|
|
|
|
// IGameEventVisitor2
|
|
virtual bool VisitString( const char* name, const char* value ) OVERRIDE
|
|
{
|
|
MLDict* pEntry = m_pEvent->add_data();
|
|
pEntry->set_key( name );
|
|
pEntry->set_val_string( value );
|
|
return true;
|
|
}
|
|
|
|
virtual bool VisitFloat( const char* name, float value ) OVERRIDE
|
|
{
|
|
MLDict* pEntry = m_pEvent->add_data();
|
|
pEntry->set_key( name );
|
|
pEntry->set_val_float( value );
|
|
return true;
|
|
}
|
|
|
|
virtual bool VisitInt( const char* name, int value ) OVERRIDE
|
|
{
|
|
MLDict* pEntry = m_pEvent->add_data();
|
|
pEntry->set_key( name );
|
|
pEntry->set_val_int( value );
|
|
return true;
|
|
}
|
|
|
|
virtual bool VisitBool( const char*name, bool value ) OVERRIDE
|
|
{
|
|
MLDict* pEntry = m_pEvent->add_data();
|
|
pEntry->set_key( name );
|
|
pEntry->set_val_int( value ? 1 : 0 );
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
MLEvent* m_pEvent;
|
|
};
|
|
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------------------------------
|
|
CCSFatDemoRecorder::CCSFatDemoRecorder()
|
|
: m_tickcount( -1 )
|
|
, m_bInLevel( false )
|
|
, m_pCurrentTick( NULL )
|
|
{
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
CCSFatDemoRecorder::~CCSFatDemoRecorder()
|
|
{
|
|
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CCSFatDemoRecorder::Reset()
|
|
{
|
|
// Sync up the state of our trackers with the current state of the game.
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CCSFatDemoRecorder::FireGameEvent( IGameEvent *pEvent )
|
|
{
|
|
if ( !csgo_fatdemo_enable.GetBool() )
|
|
return;
|
|
|
|
if ( !m_pCurrentTick )
|
|
return;
|
|
|
|
MLEvent* pOutEvent = m_pCurrentTick->add_events();
|
|
pOutEvent->set_event_name( pEvent->GetName() );
|
|
CCSFatDemoEventVisitor visitor( pOutEvent );
|
|
pEvent->ForEventData( &visitor );
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CCSFatDemoRecorder::PostInit()
|
|
{
|
|
ListenForAllGameEvents();
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CCSFatDemoRecorder::LevelInitPreEntity()
|
|
{
|
|
BeginFile();
|
|
|
|
m_bInLevel = true;
|
|
m_tickcount = -1;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CCSFatDemoRecorder::LevelShutdownPostEntity()
|
|
{
|
|
#ifdef _LINUX
|
|
bool bWasInLevel = m_bInLevel;
|
|
#endif
|
|
|
|
m_bInLevel = false;
|
|
|
|
FinalizeFile();
|
|
|
|
// Clean up our temp memory.
|
|
m_tempPacketStorage.Purge();
|
|
|
|
if ( m_pCurrentTick )
|
|
{
|
|
delete m_pCurrentTick;
|
|
m_pCurrentTick = NULL;
|
|
}
|
|
|
|
// There's an ugly crash in the bowels of scaleform that makes it hard for us to tell whether
|
|
// CSGO was actually successful or not. However, at this point we have been successful, so we
|
|
// should go ahead and exit with a success code if we're in demo_quitafterplayback mode (which
|
|
// is the usual case for autonomous capture.
|
|
#ifdef _LINUX
|
|
static ConVarRef demo_quitafterplayback( "demo_quitafterplayback" );
|
|
if ( bWasInLevel && demo_quitafterplayback.GetBool() )
|
|
{
|
|
_exit( 0 );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CCSFatDemoRecorder::OnTickPre( int tickcount )
|
|
{
|
|
if ( !csgo_fatdemo_enable.GetBool() )
|
|
return;
|
|
|
|
// Guard against multiple updates in the client if we're running a demo that isn't a timedemo.
|
|
if ( m_tickcount == tickcount )
|
|
return;
|
|
|
|
if ( !m_bInLevel )
|
|
return;
|
|
|
|
if ( !m_outFile )
|
|
return;
|
|
|
|
Assert( CSGameRules() );
|
|
|
|
if ( m_pCurrentTick )
|
|
{
|
|
m_pCurrentTick->set_tick_count( tickcount );
|
|
CaptureGameState( m_pCurrentTick->mutable_state() );
|
|
|
|
OutputProtobuf( m_pCurrentTick );
|
|
|
|
// TODO: This should be serialized or written out to a queue or something.
|
|
delete m_pCurrentTick;
|
|
m_pCurrentTick = NULL;
|
|
}
|
|
|
|
// Set up the current tick for next tick. We do this here so that any events captured from now
|
|
// until then affect the next tick (since we're done with this tick).
|
|
m_pCurrentTick = new MLTick;
|
|
|
|
// We've updated for this tick now.
|
|
m_tickcount = tickcount;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CCSFatDemoRecorder::OutputProtobuf( ::google::protobuf::Message* pProto )
|
|
{
|
|
Assert( pProto );
|
|
|
|
int32 size = pProto->ByteSize();
|
|
int32 totalSize = size + sizeof( int32 );
|
|
|
|
m_tempPacketStorage.EnsureCapacity( totalSize );
|
|
*( ( int32* ) m_tempPacketStorage.Base() ) = size;
|
|
|
|
if ( !pProto->SerializeToArray( ( ( byte* ) m_tempPacketStorage.Base() ) + sizeof( int ), size ) )
|
|
{
|
|
Assert( !"Serialization failed for... reasons." );
|
|
return;
|
|
}
|
|
|
|
g_pFullFileSystem->Write( m_tempPacketStorage.Base(), totalSize, m_outFile );
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CCSFatDemoRecorder::BeginFile()
|
|
{
|
|
char buffer[MAX_PATH];
|
|
V_strcpy_safe( buffer, csgo_fatdemo_output.GetString() );
|
|
V_DefaultExtension( buffer, ".fatdem", sizeof( buffer ) );
|
|
|
|
m_outFile = g_pFullFileSystem->OpenEx( buffer, "wb" );
|
|
if ( !m_outFile )
|
|
return;
|
|
|
|
FatDemoHeader header;
|
|
header.m_magic = MAKE_4BYTES( 'G', 'O', 'M', 'L' );
|
|
header.m_version = 1;
|
|
|
|
g_pFullFileSystem->Write( &header, sizeof( header ), m_outFile );
|
|
|
|
// Now the protobuf header,
|
|
MLDemoHeader protoHeader;
|
|
#ifdef CLIENT_DLL
|
|
protoHeader.set_map_name( engine->GetLevelNameShort() );
|
|
#else
|
|
protoHeader.set_map_name( gpGlobals->mapname.ToCStr() );
|
|
#endif
|
|
|
|
if ( gpGlobals->interval_per_tick != 0.0f )
|
|
protoHeader.set_tick_rate(1 / gpGlobals->interval_per_tick );
|
|
|
|
#ifdef CLIENT_DLL
|
|
protoHeader.set_version( engine->GetClientVersion() );
|
|
#else
|
|
protoHeader.set_version( engine->GetServerVersion() );
|
|
#endif
|
|
|
|
#ifndef NO_STEAM
|
|
EUniverse eUniverse = steamapicontext && steamapicontext->SteamUtils()
|
|
? steamapicontext->SteamUtils()->GetConnectedUniverse()
|
|
: k_EUniverseInvalid;
|
|
protoHeader.set_steam_universe( ( int ) eUniverse );
|
|
#else
|
|
// Pretty sure this doesn't actually work anymore.
|
|
protoHeader.set_steam_universe( -1 );
|
|
#endif
|
|
|
|
OutputProtobuf( &protoHeader );
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CCSFatDemoRecorder::FinalizeFile()
|
|
{
|
|
if ( m_outFile )
|
|
g_pFullFileSystem->Close( m_outFile );
|
|
m_outFile = 0;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CaptureGameState( MLGameState* pOutState )
|
|
{
|
|
CaptureMatchState( pOutState->mutable_match() );
|
|
CaptureRoundState( pOutState->mutable_round() );
|
|
|
|
for ( int i = 1; i < MAX_PLAYERS; ++i )
|
|
{
|
|
CCSPlayer* pPlayer = dynamic_cast< CCSPlayer* >( UTIL_PlayerByIndex( i ) );
|
|
|
|
if ( !pPlayer )
|
|
continue;
|
|
|
|
CapturePlayerState( pOutState->add_players(), pPlayer );
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CaptureMatchState( MLMatchState* pOutState )
|
|
{
|
|
char const *szGameMode = g_pGameTypes->GetGameModeFromInt( g_pGameTypes->GetCurrentGameType(), g_pGameTypes->GetCurrentGameMode() );
|
|
if ( !szGameMode || !*szGameMode )
|
|
szGameMode = "custom";
|
|
|
|
pOutState->set_game_mode( szGameMode );
|
|
|
|
char const *szPhase = "warmup";
|
|
bool bActivePhase = false;
|
|
if ( !CSGameRules()->IsWarmupPeriod() )
|
|
{
|
|
bActivePhase = true;
|
|
switch ( CSGameRules()->GetGamePhase() )
|
|
{
|
|
case GAMEPHASE_HALFTIME:
|
|
szPhase = "intermission";
|
|
break;
|
|
case GAMEPHASE_MATCH_ENDED:
|
|
szPhase = "gameover";
|
|
break;
|
|
default:
|
|
szPhase = "live";
|
|
break;
|
|
}
|
|
}
|
|
|
|
pOutState->set_phase( szPhase );
|
|
|
|
if ( bActivePhase )
|
|
pOutState->set_round( CSGameRules()->GetTotalRoundsPlayed() );
|
|
|
|
int nTeams[2] = { TEAM_CT, TEAM_TERRORIST };
|
|
int nScores[ 2 ] = { 0, 0 };
|
|
for ( int j = 0; j < 2; ++j )
|
|
{
|
|
auto *pTeam = GetGlobalTeam( nTeams[ j ] );
|
|
|
|
if ( !pTeam )
|
|
continue;
|
|
|
|
#ifdef CLIENT_DLL
|
|
nScores[ j ] = pTeam->Get_Score();
|
|
#else
|
|
nScores[ j ] = pTeam->GetScore();
|
|
#endif
|
|
}
|
|
|
|
pOutState->set_score_ct( nScores[ 0 ] );
|
|
pOutState->set_score_t( nScores[ 1 ] );
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CaptureRoundState( MLRoundState* pOutState )
|
|
{
|
|
char const *szPhase = "freezetime";
|
|
if ( !CSGameRules()->IsFreezePeriod() )
|
|
{
|
|
if ( CSGameRules()->IsRoundOver() )
|
|
szPhase = "over";
|
|
else
|
|
szPhase = "live";
|
|
}
|
|
pOutState->set_phase( szPhase );
|
|
|
|
switch ( CSGameRules()->m_iRoundWinStatus )
|
|
{
|
|
case WINNER_CT:
|
|
pOutState->set_win_team( ET_CT );
|
|
break;
|
|
case WINNER_TER:
|
|
pOutState->set_win_team( ET_Terrorist );
|
|
break;
|
|
}
|
|
|
|
if ( CSGameRules()->IsBombDefuseMap() )
|
|
{
|
|
char const *szBombState = "";
|
|
|
|
if ( CSGameRules()->m_bBombPlanted && !CSGameRules()->IsRoundOver() )
|
|
szBombState = "planted";
|
|
|
|
if ( CSGameRules()->IsRoundOver() )
|
|
{
|
|
// Check if the bomb exploded or got defused?
|
|
switch ( CSGameRules()->m_eRoundWinReason )
|
|
{
|
|
case Target_Bombed:
|
|
szBombState = "exploded";
|
|
break;
|
|
case Bomb_Defused:
|
|
szBombState = "defused";
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( *szBombState )
|
|
pOutState->set_bomb_state( szBombState );
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
static void DemoSetVector( CMsgVector* pOutVec, const Vector& inVec )
|
|
{
|
|
Assert( pOutVec );
|
|
pOutVec->set_x( inVec.x );
|
|
pOutVec->set_y( inVec.y );
|
|
pOutVec->set_z( inVec.z );
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
static void DemoSetQAngle( CMsgQAngle* pOutAng, const QAngle& inAng )
|
|
{
|
|
Assert( pOutAng );
|
|
pOutAng->set_x( inAng.x );
|
|
pOutAng->set_y( inAng.y );
|
|
pOutAng->set_z( inAng.z );
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
static void DemoSetQAngleAndForward( CMsgQAngle* pOutAng, CMsgVector* pOutVec, const QAngle& inAng )
|
|
{
|
|
DemoSetQAngle( pOutAng, inAng );
|
|
|
|
Vector fwd;
|
|
AngleVectors( inAng, &fwd );
|
|
DemoSetVector( pOutVec, fwd );
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CapturePlayerState( MLPlayerState* pOutState, CCSPlayer* pCsPlayer )
|
|
{
|
|
CSteamID steamID;
|
|
if ( pCsPlayer->GetSteamID( &steamID ) )
|
|
pOutState->set_account_id( steamID.GetAccountID() );
|
|
|
|
pOutState->set_entindex( pCsPlayer->entindex() );
|
|
|
|
pOutState->set_name( pCsPlayer->GetPlayerName() );
|
|
// pOutState->set_clan( );
|
|
pOutState->set_team( ( ETeam )( pCsPlayer->GetTeamNumber() ) );
|
|
pOutState->set_user_id( pCsPlayer->GetUserID() );
|
|
|
|
DemoSetVector( pOutState->mutable_abspos(), pCsPlayer->GetAbsOrigin() );
|
|
DemoSetQAngleAndForward( pOutState->mutable_eyeangle(), pOutState->mutable_eyeangle_fwd(), pCsPlayer->EyeAngles() );
|
|
|
|
pOutState->set_health( pCsPlayer->GetHealth() );
|
|
pOutState->set_armor( pCsPlayer->ArmorValue() );
|
|
#ifdef CLIENT_DLL
|
|
pOutState->set_flashed( clamp( pCsPlayer->m_flFlashOverlayAlpha, 0.0f, 1.0f ) );
|
|
pOutState->set_smoked( clamp( pCsPlayer->GetLastSmokeOverlayAlpha(), 0.0f, 1.0f ) );
|
|
pOutState->set_money( pCsPlayer->GetAccount() );
|
|
pOutState->set_helmet( pCsPlayer->HasHelmet() );
|
|
#else
|
|
// TODO pOutState->set_flashed( clamp( pCsPlayer->m_flFlashOverlayAlpha, 0.0f, 1.0f ) );
|
|
// TODO pOutState->set_smoked( clamp( pCsPlayer->GetLastSmokeOverlayAlpha(), 0.0f, 1.0f ) );
|
|
pOutState->set_money( pCsPlayer->m_iAccount );
|
|
pOutState->set_helmet( pCsPlayer->m_bHasHelmet );
|
|
|
|
#endif
|
|
pOutState->set_round_kills( pCsPlayer->m_iNumRoundKills );
|
|
pOutState->set_round_killhs( pCsPlayer->m_iNumRoundKillsHeadshots );
|
|
|
|
pOutState->set_defuse_kit( pCsPlayer->HasDefuser() );
|
|
|
|
float flOnFireAmount = 0.0f;
|
|
if ( ( pCsPlayer->m_fMolotovDamageTime > 0.0f ) && ( gpGlobals->curtime - pCsPlayer->m_fMolotovDamageTime < 2 ) ) // took burn damage in last two seconds
|
|
{
|
|
flOnFireAmount = ( gpGlobals->curtime - pCsPlayer->m_fMolotovDamageTime <= 1.0f )
|
|
? 1.0f
|
|
: ( 2.0f - gpGlobals->curtime + pCsPlayer->m_fMolotovDamageTime );
|
|
}
|
|
pOutState->set_burning( clamp( flOnFireAmount, 0.0f, 1.0f ) );
|
|
|
|
int numWeapons = 0;
|
|
for ( int i = 0; i < pCsPlayer->WeaponCount(); ++i )
|
|
{
|
|
CWeaponCSBase* pCsWeapon = dynamic_cast< CWeaponCSBase * >( pCsPlayer->GetWeapon( i ) );
|
|
if ( !pCsWeapon )
|
|
continue;
|
|
|
|
CaptureWeaponState( pOutState->add_weapons(), pCsWeapon, numWeapons, pCsPlayer );
|
|
++numWeapons;
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
void CaptureWeaponState( MLWeaponState* pOutState, CWeaponCSBase* pCsWeapon, int index, CCSPlayer* pCsPlayer )
|
|
{
|
|
pOutState->set_index( index );
|
|
pOutState->set_name( pCsWeapon->GetName() );
|
|
pOutState->set_type( ( EWeaponType ) pCsWeapon->GetWeaponType() );
|
|
pOutState->set_recoil_index( pCsWeapon->m_flRecoilIndex );
|
|
|
|
if ( pCsWeapon->m_iClip1 >= 0 )
|
|
pOutState->set_ammo_clip( pCsWeapon->m_iClip1 );
|
|
|
|
int iMaxClip1 = pCsWeapon->GetMaxClip1();
|
|
if ( iMaxClip1 > 0 )
|
|
pOutState->set_ammo_clip_max( iMaxClip1 );
|
|
|
|
if ( pCsWeapon->GetPrimaryAmmoType() >= 0 )
|
|
pOutState->set_ammo_reserve( pCsWeapon->GetReserveAmmoCount( AMMO_POSITION_PRIMARY ) );
|
|
|
|
char const *szState = "holstered";
|
|
if ( pCsPlayer->GetActiveCSWeapon() == pCsWeapon )
|
|
{
|
|
szState = "active";
|
|
if ( pCsWeapon->m_bInReload )
|
|
szState = "reloading";
|
|
}
|
|
pOutState->set_state( szState );
|
|
}
|
|
|
|
#endif
|