Team Fortress 2 Source Code as on 22/4/2020
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.

601 lines
18 KiB

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose:
  4. //
  5. //=====================================================================================//
  6. #include "cbase.h"
  7. #include "filesystem.h"
  8. #include "gcsdk/webapi_response.h"
  9. #include "c_tf_player.h"
  10. #include "tf_weapon_medigun.h"
  11. #include "tf_demo_support.h"
  12. #include "tf_gamerules.h"
  13. #include "tf_hud_chat.h"
  14. #include "vguicenterprint.h"
  15. // Global singleton
  16. static CTFDemoSupport g_DemoSupport;
  17. extern ConVar mp_tournament;
  18. ConVar ds_enable( "ds_enable", "0", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_ARCHIVE, "Demo support - enable automatic .dem file recording and features. 0 - Manual, 1 - Auto-record competitive matches, 2 - Auto-record all matches, 3 - Auto-record tournament (mp_tournament) matches", true, 0, true, 3 );
  19. ConVar ds_dir( "ds_dir", "demos", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_ARCHIVE, "Demo support - will put all files into this folder under the gamedir. 24 characters max." );
  20. ConVar ds_prefix( "ds_prefix", "", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_ARCHIVE, "Demo support - will prefix files with this string. 24 characters max." );
  21. ConVar ds_min_streak( "ds_min_streak", "4", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_ARCHIVE, "Demo support - minimum kill streak count before being recorded.", true, 2, false, 0 );
  22. ConVar ds_kill_delay( "ds_kill_delay", "15", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_ARCHIVE, "Demo support - maximum time between kills for tracking kill streaks.", true, 5, false, 0 );
  23. ConVar ds_log( "ds_log", "1", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_ARCHIVE, "Demo support - log kill streak and bookmark events to an associated .txt file.", true, 0, true, 1 );
  24. ConVar ds_sound( "ds_sound", "1", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_ARCHIVE, "Demo support - play start/stop sound for demo recording.", true, 0, true, 1 );
  25. ConVar ds_notify( "ds_notify", "0", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_ARCHIVE, "Demo support - text output when recording start/stop/bookmark events : 0 - console, 1 - console and chat, 2 - console and HUD.", true, 0, true, 2 );
  26. ConVar ds_screens( "ds_screens", "1", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_ARCHIVE, "Demo support - take screenshot of the scoreboard for non-competitive matches or the match summary stats for competitive matches. For competitive matches, it will not capture the screenshot if you disconnect from the server before the medal awards have completed.", true, 0, true, 1 );
  27. ConVar ds_autodelete( "ds_autodelete", "0", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_ARCHIVE, "Demo support - automatically delete .dem files with no associated bookmark or kill streak events.", true, 0, true, 1 );
  28. CON_COMMAND_F( ds_mark, "Demo support - bookmark (with optional single-word description) the current tick count for the demo being recorded.", FCVAR_CLIENTDLL | FCVAR_DONTRECORD )
  29. {
  30. g_DemoSupport.BookMarkCurrentTick( ( args.ArgC() > 1 ) ? args[1] : NULL );
  31. }
  32. CON_COMMAND_F( ds_record, "Demo support - start recording a demo.", FCVAR_CLIENTDLL | FCVAR_DONTRECORD )
  33. {
  34. g_DemoSupport.StartRecording();
  35. }
  36. CON_COMMAND_F( ds_stop, "Demo support - stop recording a demo.", FCVAR_CLIENTDLL | FCVAR_DONTRECORD )
  37. {
  38. g_DemoSupport.StopRecording();
  39. }
  40. CON_COMMAND_F( ds_status, "Demo support - show the current recording status.", FCVAR_CLIENTDLL | FCVAR_DONTRECORD )
  41. {
  42. g_DemoSupport.Status();
  43. }
  44. //-----------------------------------------------------------------------------
  45. // Purpose:
  46. //-----------------------------------------------------------------------------
  47. static const char *g_aDemoEventNames[] =
  48. {
  49. "Bookmark",
  50. "Killstreak",
  51. };
  52. COMPILE_TIME_ASSERT( ARRAYSIZE( g_aDemoEventNames ) == eDemoEvent_Last );
  53. const char *GetDemoEventName( EDemoEventType eEventType )
  54. {
  55. if ( ( eEventType >= ARRAYSIZE( g_aDemoEventNames ) ) || ( eEventType < 0 ) )
  56. return NULL;
  57. return g_aDemoEventNames[eEventType];
  58. }
  59. //-----------------------------------------------------------------------------
  60. // Purpose:
  61. //-----------------------------------------------------------------------------
  62. CTFDemoSupport::CTFDemoSupport() : CAutoGameSystemPerFrame( "CTFDemoSupport" )
  63. {
  64. m_nKillCount = 0;
  65. m_flLastKill = -1.f;
  66. m_bRecording = false;
  67. m_hGlobalEventList = FILESYSTEM_INVALID_HANDLE;
  68. m_DemoSpecificEventList.Clear();
  69. m_DemoSpecificEventList.SetStatusCode( k_EHTTPStatusCode200OK );
  70. m_pRoot = NULL;
  71. m_pChildArray = NULL;
  72. m_flScreenshotTime = -1.f;
  73. m_bAlreadyAutoRecordedOnce = false;
  74. m_flNextRecordStartCheckTime = -1.f;
  75. m_bFirstEvent = false;
  76. m_nStartingTickCount = 0;
  77. m_bHasAtLeastOneEvent = false;
  78. }
  79. //-----------------------------------------------------------------------------
  80. // Purpose:
  81. //-----------------------------------------------------------------------------
  82. bool CTFDemoSupport::Init()
  83. {
  84. ListenForGameEvent( "localplayer_respawn" );
  85. ListenForGameEvent( "player_death" );
  86. ListenForGameEvent( "client_disconnect" );
  87. ListenForGameEvent( "ds_screenshot" );
  88. ListenForGameEvent( "ds_stop" );
  89. return true;
  90. }
  91. //-----------------------------------------------------------------------------
  92. // Purpose:
  93. //-----------------------------------------------------------------------------
  94. void CTFDemoSupport::LevelInitPostEntity()
  95. {
  96. if ( engine->IsPlayingDemo() )
  97. return;
  98. m_bAlreadyAutoRecordedOnce = false;
  99. m_flNextRecordStartCheckTime = -1.f;
  100. }
  101. //-----------------------------------------------------------------------------
  102. // Purpose:
  103. //-----------------------------------------------------------------------------
  104. void CTFDemoSupport::LevelShutdownPostEntity()
  105. {
  106. if ( engine->IsPlayingDemo() )
  107. return;
  108. StopRecording();
  109. }
  110. //-----------------------------------------------------------------------------
  111. // Purpose:
  112. //-----------------------------------------------------------------------------
  113. void CTFDemoSupport::Update( float frametime )
  114. {
  115. if ( engine->IsPlayingDemo() )
  116. return;
  117. if ( ds_enable.GetInt() > 0 )
  118. {
  119. if ( !m_bRecording && !m_bAlreadyAutoRecordedOnce )
  120. {
  121. if ( ds_enable.GetInt() == 1 )
  122. {
  123. if ( TFGameRules() && !TFGameRules()->IsCompetitiveMode() )
  124. return;
  125. }
  126. else if ( ds_enable.GetInt() == 3 )
  127. {
  128. if ( !mp_tournament.GetBool() )
  129. return;
  130. }
  131. if ( ( m_flNextRecordStartCheckTime < 0 ) || ( m_flNextRecordStartCheckTime < gpGlobals->curtime ) )
  132. {
  133. CTFPlayer *pLocalPlayer = CTFPlayer::GetLocalTFPlayer();
  134. if ( pLocalPlayer )
  135. {
  136. // if the local player is on team spectator or is on a game team and has picked a player class
  137. if ( ( pLocalPlayer->GetTeamNumber() == TEAM_SPECTATOR ) ||
  138. ( ( pLocalPlayer->GetTeamNumber() >= FIRST_GAME_TEAM ) && pLocalPlayer->GetPlayerClass() && ( pLocalPlayer->GetPlayerClass()->GetClassIndex() >= TF_FIRST_NORMAL_CLASS ) && ( pLocalPlayer->GetPlayerClass()->GetClassIndex() < TF_LAST_NORMAL_CLASS ) ) )
  139. {
  140. if ( !StartRecording() )
  141. {
  142. // we'll try again in 5 seconds
  143. m_flNextRecordStartCheckTime = gpGlobals->curtime + 5.f;
  144. return;
  145. }
  146. }
  147. }
  148. }
  149. }
  150. }
  151. if ( !m_bRecording )
  152. return;
  153. if ( ( m_flScreenshotTime > 0 ) && ( m_flScreenshotTime < gpGlobals->curtime ) )
  154. {
  155. m_flScreenshotTime = -1.f;
  156. if ( ds_screens.GetBool() )
  157. {
  158. engine->TakeScreenshot( m_szFilename, m_szFolder );
  159. Notify( "(Demo Support) Screenshot saved\n" );
  160. }
  161. }
  162. }
  163. //-----------------------------------------------------------------------------
  164. // Purpose:
  165. //-----------------------------------------------------------------------------
  166. void CTFDemoSupport::Status( void )
  167. {
  168. if ( engine->IsPlayingDemo() )
  169. return;
  170. char szStatus[64] = {0};
  171. if ( m_bRecording )
  172. {
  173. V_sprintf_safe( szStatus, "(Demo Support) Currently recording to %s\n", m_szFolderAndFilename );
  174. }
  175. else
  176. {
  177. V_strcpy_safe( szStatus, "(Demo Support) Not currently recording\n" );
  178. }
  179. Notify( szStatus );
  180. }
  181. //-----------------------------------------------------------------------------
  182. // Purpose:
  183. //-----------------------------------------------------------------------------
  184. void CTFDemoSupport::Notify( char *pszMessage )
  185. {
  186. if ( engine->IsPlayingDemo() )
  187. return;
  188. if ( pszMessage && pszMessage[0] )
  189. {
  190. // we'll always put the message in the console
  191. Msg( "%s", pszMessage );
  192. switch ( ds_notify.GetInt() )
  193. {
  194. default: // console
  195. case 0:
  196. break;
  197. case 1: // chat window
  198. {
  199. CBaseHudChat *pHUDChat = (CBaseHudChat *)GET_HUDELEMENT( CHudChat );
  200. if ( pHUDChat )
  201. {
  202. pHUDChat->Printf( CHAT_FILTER_NONE, "%s", pszMessage );
  203. }
  204. }
  205. break;
  206. case 2: // hud center print
  207. internalCenterPrint->Print( pszMessage );
  208. break;
  209. }
  210. }
  211. }
  212. //-----------------------------------------------------------------------------
  213. // Purpose:
  214. //-----------------------------------------------------------------------------
  215. void CTFDemoSupport::LogEvent( EDemoEventType eType, int nValue /* = 0 */, const char *pszValue /* = NULL */ )
  216. {
  217. if ( engine->IsPlayingDemo() )
  218. return;
  219. if ( !m_bRecording )
  220. return;
  221. if ( !ds_log.GetBool() )
  222. return;
  223. char szArg[32] = {0};
  224. switch ( eType )
  225. {
  226. case eDemoEvent_Bookmark:
  227. {
  228. const char *pszTemp = "General";
  229. if ( pszValue && pszValue[0] )
  230. {
  231. pszTemp = pszValue;
  232. }
  233. V_strcpy_safe( szArg, pszTemp );
  234. }
  235. break;
  236. case eDemoEvent_Killstreak:
  237. V_sprintf_safe( szArg, "%d", nValue );
  238. break;
  239. default:
  240. // don't continue if this is an unknown type
  241. return;
  242. }
  243. time_t tTime = CRTime::RTime32TimeCur();
  244. struct tm tmStruct;
  245. struct tm *ptm = Plat_localtime( &tTime, &tmStruct );
  246. int nTickCount = gpGlobals->tickcount - m_nStartingTickCount;
  247. if ( m_hGlobalEventList != FILESYSTEM_INVALID_HANDLE )
  248. {
  249. if ( m_bFirstEvent )
  250. {
  251. m_bFirstEvent = false;
  252. g_pFullFileSystem->FPrintf( m_hGlobalEventList, ">\n" );
  253. }
  254. g_pFullFileSystem->FPrintf( m_hGlobalEventList, "[%04u/%02u/%02u %02u:%02u] %s %s (\"%s\" at %d)\n",
  255. ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday, ptm->tm_hour, ptm->tm_min, g_aDemoEventNames[eType], szArg, m_szFilename, nTickCount );
  256. }
  257. if ( m_pChildArray )
  258. {
  259. GCSDK::CWebAPIValues *pChildObject = m_pChildArray->AddChildObjectToArray();
  260. pChildObject->CreateChildObject( "name" )->SetStringValue( g_aDemoEventNames[eType] );
  261. pChildObject->CreateChildObject( "value" )->SetStringValue( szArg );
  262. pChildObject->CreateChildObject( "tick" )->SetInt32Value( nTickCount );
  263. m_bHasAtLeastOneEvent = true;
  264. }
  265. char szMessage[MAX_PATH] = {0};
  266. V_sprintf_safe( szMessage, "(Demo Support) Event recorded: %s %s\n", g_aDemoEventNames[eType], szArg );
  267. Notify( szMessage );
  268. }
  269. //-----------------------------------------------------------------------------
  270. // Purpose:
  271. //-----------------------------------------------------------------------------
  272. void CTFDemoSupport::FireGameEvent( IGameEvent * event )
  273. {
  274. if ( engine->IsPlayingDemo() )
  275. return;
  276. if ( !m_bRecording )
  277. return;
  278. const char *pszEvent = event->GetName();
  279. if ( FStrEq( pszEvent, "localplayer_respawn" ) )
  280. {
  281. m_nKillCount = 0;
  282. m_flLastKill = -1.f;
  283. }
  284. else if ( FStrEq( pszEvent, "player_death" ) )
  285. {
  286. if ( m_bRecording )
  287. {
  288. CBasePlayer *pLocalPlayer = CBasePlayer::GetLocalPlayer();
  289. if ( !pLocalPlayer )
  290. return;
  291. int nOldKillCount = m_nKillCount;
  292. int nLocalPlayerUserID = pLocalPlayer->GetUserID();
  293. if ( nLocalPlayerUserID == event->GetInt( "userid" ) )
  294. {
  295. // local player was the victim
  296. m_nKillCount = 0;
  297. }
  298. else if ( nLocalPlayerUserID == event->GetInt( "attacker" ) )
  299. {
  300. // local player was the killer
  301. if ( ( m_flLastKill < 0 ) || ( gpGlobals->curtime - m_flLastKill > ds_kill_delay.GetFloat() ) )
  302. {
  303. m_nKillCount = 1;
  304. }
  305. else
  306. {
  307. m_nKillCount++;
  308. }
  309. m_flLastKill = gpGlobals->curtime;
  310. }
  311. else if ( nLocalPlayerUserID == event->GetInt( "assister" ) )
  312. {
  313. CTFPlayer *pTFPlayer = ToTFPlayer( pLocalPlayer );
  314. if ( pTFPlayer )
  315. {
  316. if ( pTFPlayer->IsPlayerClass( TF_CLASS_MEDIC ) && pLocalPlayer->GetActiveWeapon() )
  317. {
  318. CWeaponMedigun *pMedigun = dynamic_cast<CWeaponMedigun*>( pLocalPlayer->GetActiveWeapon() );
  319. if ( pMedigun )
  320. {
  321. // local player was a medic healing the attacker so give them credit
  322. if ( ( m_flLastKill < 0 ) || ( gpGlobals->curtime - m_flLastKill > ds_kill_delay.GetFloat() ) )
  323. {
  324. m_nKillCount = 1;
  325. }
  326. else
  327. {
  328. m_nKillCount++;
  329. }
  330. m_flLastKill = gpGlobals->curtime;
  331. }
  332. }
  333. }
  334. }
  335. // if our kill-streak has increased, make an event entry
  336. if ( ( nOldKillCount != m_nKillCount ) && ( m_nKillCount > 0 ) && ( m_nKillCount >= ds_min_streak.GetInt() ) )
  337. {
  338. LogEvent( eDemoEvent_Killstreak, m_nKillCount );
  339. }
  340. }
  341. }
  342. else if ( FStrEq( pszEvent, "client_disconnect" ) )
  343. {
  344. StopRecording();
  345. }
  346. else if ( FStrEq( pszEvent, "ds_stop" ) )
  347. {
  348. StopRecording( true );
  349. }
  350. else if ( FStrEq( pszEvent, "ds_screenshot" ) )
  351. {
  352. if ( ds_screens.GetBool() )
  353. {
  354. float flDelay = event->GetFloat( "delay" );
  355. m_flScreenshotTime = gpGlobals->curtime + flDelay;
  356. }
  357. }
  358. }
  359. //-----------------------------------------------------------------------------
  360. // Purpose:
  361. //-----------------------------------------------------------------------------
  362. void CTFDemoSupport::BookMarkCurrentTick( const char *pszValue /* = NULL */ )
  363. {
  364. if ( engine->IsPlayingDemo() )
  365. return;
  366. LogEvent( eDemoEvent_Bookmark, 0, pszValue );
  367. }
  368. //-----------------------------------------------------------------------------
  369. // Purpose:
  370. //-----------------------------------------------------------------------------
  371. bool CTFDemoSupport::IsValidPath( const char *pszFolder )
  372. {
  373. if ( !pszFolder )
  374. return false;
  375. if ( Q_strlen( pszFolder ) <= 0 ||
  376. Q_strstr( pszFolder, "\\\\" ) || // to protect network paths
  377. Q_strstr( pszFolder, ":" ) || // to protect absolute paths
  378. Q_strstr( pszFolder, ".." ) || // to protect relative paths
  379. Q_strstr( pszFolder, "\n" ) || // CFileSystem_Stdio::FS_fopen doesn't allow this
  380. Q_strstr( pszFolder, "\r" ) ) // CFileSystem_Stdio::FS_fopen doesn't allow this
  381. {
  382. return false;
  383. }
  384. return true;
  385. }
  386. //-----------------------------------------------------------------------------
  387. // Purpose:
  388. //-----------------------------------------------------------------------------
  389. bool CTFDemoSupport::StartRecording( void )
  390. {
  391. if ( engine->IsPlayingDemo() )
  392. return false;
  393. // are we already recording?
  394. if ( m_bRecording )
  395. {
  396. Notify( "(Demo Support) Already recording\n" );
  397. return false;
  398. }
  399. // start recording the demo
  400. char szTime[k_RTimeRenderBufferSize] = {0};
  401. time_t tTime = CRTime::RTime32TimeCur();
  402. struct tm tmStruct;
  403. struct tm *ptm = Plat_localtime( &tTime, &tmStruct );
  404. V_sprintf_safe( szTime, "%04u-%02u-%02u_%02u-%02u-%02u",
  405. ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday,
  406. ptm->tm_hour, ptm->tm_min, ptm->tm_sec );
  407. char szPrefix[24] = {0};
  408. V_sprintf_safe( szPrefix, "%s", ds_prefix.GetString() );
  409. V_sprintf_safe( m_szFilename, "%s%s", szPrefix, szTime );
  410. if ( Q_strlen( ds_dir.GetString() ) > 0 )
  411. {
  412. // check folder
  413. if ( !IsValidPath( ds_dir.GetString() ) )
  414. {
  415. Msg( "DemoSupport: invalid folder.\n" );
  416. return false;
  417. }
  418. V_sprintf_safe( m_szFolder, "%s", ds_dir.GetString() );
  419. // make sure the folder exists
  420. g_pFullFileSystem->CreateDirHierarchy( m_szFolder, "GAME" );
  421. V_sprintf_safe( m_szFolderAndFilename, "%s%c%s", m_szFolder, CORRECT_PATH_SEPARATOR, m_szFilename );
  422. }
  423. else
  424. {
  425. m_szFolder[0] = '\0';
  426. V_sprintf_safe( m_szFolderAndFilename, "%s", m_szFilename );
  427. }
  428. if ( !engine->StartDemoRecording( m_szFilename, m_szFolder ) )
  429. {
  430. Notify( "(Demo Support) Unable to start recording\n" );
  431. return false;
  432. }
  433. char szGobalFile[MAX_PATH] = { 0 };
  434. V_sprintf_safe( szGobalFile, "%s%c%s", m_szFolder, CORRECT_PATH_SEPARATOR, EVENTS_FILENAME );
  435. m_hGlobalEventList = g_pFullFileSystem->Open( szGobalFile, "at", "GAME" );
  436. m_bFirstEvent = true;
  437. m_DemoSpecificEventList.Clear();
  438. m_DemoSpecificEventList.SetStatusCode( k_EHTTPStatusCode200OK );
  439. m_pRoot = m_DemoSpecificEventList.CreateRootValue( "summary" );
  440. m_DemoSpecificEventList.SetJSONAnonymousRootNode( true );
  441. m_pChildArray = m_pRoot->CreateChildArray( "events", "event" );
  442. m_bRecording = true;
  443. m_bAlreadyAutoRecordedOnce = true;
  444. m_nStartingTickCount = gpGlobals->tickcount;
  445. m_bHasAtLeastOneEvent = false;
  446. if ( ds_sound.GetBool() )
  447. {
  448. CBasePlayer *pLocalPlayer = CBasePlayer::GetLocalPlayer();
  449. if ( pLocalPlayer )
  450. {
  451. pLocalPlayer->EmitSound( "DemoSupport.StartRecording" );
  452. }
  453. }
  454. char szMessage[MAX_PATH] = { 0 };
  455. V_sprintf_safe( szMessage, "(Demo Support) Start recording %s\n", m_szFolderAndFilename );
  456. Notify( szMessage );
  457. return true;
  458. }
  459. //-----------------------------------------------------------------------------
  460. // Purpose:
  461. //-----------------------------------------------------------------------------
  462. void CTFDemoSupport::StopRecording( bool bFromEngine /* = false */ )
  463. {
  464. if ( engine->IsPlayingDemo() )
  465. return;
  466. if ( !m_bRecording )
  467. return;
  468. m_bRecording = false;
  469. m_nStartingTickCount = 0;
  470. // stop recording the demo
  471. if ( !bFromEngine )
  472. {
  473. engine->StopDemoRecording();
  474. }
  475. if ( ds_sound.GetBool() )
  476. {
  477. CBasePlayer *pLocalPlayer = CBasePlayer::GetLocalPlayer();
  478. if ( pLocalPlayer )
  479. {
  480. pLocalPlayer->EmitSound( "DemoSupport.EndRecording" );
  481. }
  482. }
  483. char szMessage[MAX_PATH] = { 0 };
  484. V_sprintf_safe( szMessage, "(Demo Support) End recording %s\n", m_szFolderAndFilename );
  485. Notify( szMessage );
  486. if ( m_hGlobalEventList != FILESYSTEM_INVALID_HANDLE )
  487. {
  488. g_pFullFileSystem->Close( m_hGlobalEventList );
  489. m_bFirstEvent = true;
  490. }
  491. if ( ds_autodelete.GetBool() && !m_bHasAtLeastOneEvent )
  492. {
  493. char szTemp[MAX_PATH] = { 0 };
  494. V_sprintf_safe( szTemp, "%s.dem", m_szFolderAndFilename );
  495. g_pFullFileSystem->RemoveFile( szTemp, "GAME" );
  496. V_sprintf_safe( szMessage, "(Demo Support) Auto-delete recording %s\n", m_szFolderAndFilename );
  497. Notify( szMessage );
  498. }
  499. else
  500. {
  501. if ( ds_log.GetBool() )
  502. {
  503. // write out the associated bookmark and kill-streak data file
  504. char szTempFilename[MAX_PATH] = {0};
  505. V_sprintf_safe( szTempFilename, "%s.json", m_szFolderAndFilename );
  506. CUtlBuffer buffer( 0, 0, CUtlBuffer::TEXT_BUFFER | CUtlBuffer::EXTERNAL_GROWABLE );
  507. m_DemoSpecificEventList.BEmitFormattedOutput( GCSDK::k_EWebAPIOutputFormat_JSON, buffer, 0 );
  508. g_pFullFileSystem->WriteFile( szTempFilename, "GAME", buffer );
  509. }
  510. }
  511. m_DemoSpecificEventList.Clear();
  512. m_pRoot = NULL;
  513. m_pChildArray = NULL;
  514. m_bHasAtLeastOneEvent = false;
  515. }