//=========== 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