source-engine/utils/tf_monoculus_stats/tf_monoculus_stats.cpp
FluorescentCIAAfricanAmerican 3bf9df6b27 1
2020-04-22 12:56:21 -04:00

723 lines
15 KiB
C++

//========= Copyright Valve Corporation, All rights reserved. ============//
// Parse Halloween 2011 server logs and mine Eyeball Boss (Monoculus) data
#define _CRT_SECURE_NO_WARNINGS // STFU
#include <tchar.h>
#include "stdio.h"
#include <math.h>
#include <vector>
#define MAX_BUFFER_SIZE 256
#define MAX_NAME_SIZE 64
enum EventType
{
EYEBALL_SPAWN, // eyeball_spawn (max_health 11200) (player_count 18)
EYEBALL_ESCAPE, // eyeball_escaped (max_dps 220.00) (health 3071)
EYEBALL_DEATH, // eyeball_death (max_dps 467.63) (max_health 12000) (player_count 23)
EYEBALL_STUN, // "Aque0us<174><STEAM_0:0:212532><Red>" eyeball_stunned with "NoWeapon" (attacker_position "-1033 872 143")
PURGATORY_ENTER, // "I_DONT_KNOW<181><STEAM_0:1:34210475><Red>" purgatory_teleport "spawn_purgatory"
PURGATORY_ESCAPE, // "I_DONT_KNOW<181><STEAM_0:1:34210475><Red>" purgatory_escaped
LOOT_ISLAND_ENTER, // "I_DONT_KNOW<181><STEAM_0:1:34210475><Red>" purgatory_teleport "spawn_loot"
EYEBALL_KILLER, // "Spaghetti Western<192><STEAM_0:1:41812531><Red>" eyeball_killer with "liberty_launcher" (attacker_position "-1629 -293 213")
EVENT_COUNT
};
#define TEAM_NONE 0
#define TEAM_RED 1
#define TEAM_BLUE 2
struct EventInfo
{
EventType m_type;
char m_date[ MAX_NAME_SIZE ];
int m_timestamp; // seconds since log start
int m_health;
int m_maxHealth;
int m_playerCount;
int m_maxDPS;
int m_attackerPosX;
int m_attackerPosY;
int m_attackerPosZ;
char m_weaponName[ MAX_NAME_SIZE ];
char m_playerName[ MAX_NAME_SIZE ];
int m_playerID;
int m_team;
};
std::vector< EventInfo > TheData;
//----------------------------------------------------------------------------------
bool ParseEvent( FILE *fp, EventInfo *event )
{
return false;
}
//----------------------------------------------------------------------------------
char *ParsePlayer( EventInfo *event, char *data )
{
// "TheSauce<17><STEAM_0:1:19333174><Red>"
// ARGH! Player names can contain '<' and '>'
// skip the "
++data;
char *restOfData = NULL;
char *c = &data[ strlen(data)-1 ];
// backup until we find '>'
while( c != data )
{
--c;
if ( *c == '>' )
{
restOfData = c+2;
// terminate the team name
*c = '\000';
break;
}
}
// backup until we find '<'
char *teamName = NULL;
while( c != data )
{
--c;
if ( *c == '<' )
{
teamName = c+1;
// back up and terminate the Steam ID
--c;
*c = '\000';
break;
}
}
if ( !teamName )
{
return NULL;
}
if ( !strcmp( "Red", teamName ) )
{
event->m_team = TEAM_RED;
}
else if ( !strcmp( "Blue", teamName ) )
{
event->m_team = TEAM_BLUE;
}
else
{
return NULL;
}
// "TheSauce<17><STEAM_0:1:19333174><Red>"
// backup until we find '<'
char *steamID = NULL;
while( c != data )
{
--c;
if ( *c == '<' )
{
steamID = c+1;
// back up and terminate the player ID
--c;
*c = '\000';
break;
}
}
if ( !steamID )
{
return NULL;
}
// backup until we find '<'
char *playerID = NULL;
while( c != data )
{
--c;
if ( *c == '<' )
{
playerID = c+1;
// terminate the player name
*c = '\000';
break;
}
}
if ( !playerID )
{
return NULL;
}
event->m_playerID = atoi( playerID );
strcpy( event->m_playerName, data );
return restOfData;
#ifdef OLDWAY
// skip the "
++data;
char *token = strtok( data, "<" );
if ( !token )
return NULL;
strcpy( event->m_playerName, token );
token = strtok( NULL, ">" );
if ( !token )
return NULL;
event->m_playerID = atoi( token );
// steam ID
token = strtok( NULL, "<>" );
if ( !token )
return NULL;
// team
token = strtok( NULL, "<>" );
if ( !token )
return NULL;
if ( !strcmp( "Red", token ) )
{
event->m_team = TEAM_RED;
}
else if ( !strcmp( "Blue", token ) )
{
event->m_team = TEAM_BLUE;
}
// get rest of line after closing quote
token = strtok( NULL, "" );
token += 2;
return token;
#endif
}
//----------------------------------------------------------------------------------
// Parse and return int value from "(name value)"
bool parse_strtok_int( const char *name, int *value )
{
char *token = strtok( NULL, "( " );
if ( !token )
return false;
if ( _stricmp( name, token ) )
return false;
// get value
token = strtok( NULL, ") " );
if ( !token )
return false;
*value = atoi( token );
return true;
}
//----------------------------------------------------------------------------------
bool ProcessData( const char *filename )
{
FILE *fp = fopen( filename, "r" );
if ( !fp )
{
printf( "ERROR: Cannot access file '%s'\n", filename );
return false;
}
FILE *errorFP = fopen( "cooked_stats_error.txt", "w" );
if ( !errorFP )
{
printf( "ERROR: Can't open output file\n" );
return false;
}
char buffer[ MAX_BUFFER_SIZE ];
bool isFirstLine = true;
int initialTimestamp = 0;
int line = 0;
EventInfo info;
while( true )
{
memset( &info, 0, sizeof( EventInfo ) );
fgets( buffer, MAX_BUFFER_SIZE, fp );
++line;
// eat filename
char *token = buffer;
while( *token != 'L' )
{
++token;
}
if ( !token )
break;
// read date
token = strtok( token, "L " );
if ( !token )
break;
strcpy( info.m_date, token );
// read time
token = strtok( NULL, "- " );
if ( !token )
break;
token[2] = '\000';
int hour = atoi( token );
token[5] = '\000';
int minute = atoi( &token[3] );
token[8] = '\000';
int second = atoi( &token[6] );
int timestamp = hour * 3600 + minute * 60 + second;
if ( isFirstLine )
{
isFirstLine = false;
initialTimestamp = timestamp;
}
info.m_timestamp = timestamp - initialTimestamp;
int fullDay = 24 * 3600;
if ( info.m_timestamp > fullDay )
{
// wrapped past midnight
info.m_timestamp -= fullDay;
}
// eat "HALLOWEEN"
token = strtok( NULL, ": " );
if ( !token )
break;
// get the rest of the line
token = strtok( NULL, "" );
if ( !token )
break;
// skip trailing space
++token;
if ( !strncmp( "eyeball_spawn", token, 13 ) )
{
// eyeball_spawn (max_health 8000) (player_count 10)
info.m_type = EYEBALL_SPAWN;
// eat 'eyeball_spawn'
token = strtok( token, " " );
if ( !token )
break;
if ( !parse_strtok_int( "max_health", &info.m_maxHealth ) )
break;
if ( !parse_strtok_int( "player_count", &info.m_playerCount ) )
break;
}
else if ( !strncmp( "eyeball_escaped", token, 15 ) )
{
// eyeball_escaped (max_dps 143.64) (health 1525)
info.m_type = EYEBALL_ESCAPE;
// eat 'eyeball_escape'
token = strtok( token, " " );
if ( !token )
break;
if ( !parse_strtok_int( "max_dps", &info.m_maxDPS ) )
break;
if ( !parse_strtok_int( "health", &info.m_health ) )
break;
}
else if ( !strncmp( "eyeball_death", token, 13 ) )
{
// eyeball_death (max_dps 285.54) (max_health 13200) (player_count 24)
info.m_type = EYEBALL_DEATH;
// eat 'eyeball_death'
token = strtok( token, " " );
if ( !token )
break;
if ( !parse_strtok_int( "max_dps", &info.m_maxDPS ) )
break;
if ( !parse_strtok_int( "max_health", &info.m_maxHealth ) )
break;
if ( !parse_strtok_int( "player_count", &info.m_playerCount ) )
break;
}
else if ( token[0] == '"' )
{
char *data = ParsePlayer( &info, token );
if ( data )
{
token = strtok( data, " " );
if ( !token )
continue;
if ( !strncmp( "purgatory_escaped", token, 17 ) )
{
info.m_type = PURGATORY_ESCAPE;
}
else if ( !strcmp( "purgatory_teleport", token ) )
{
token = strtok( NULL, "\" " );
if ( !token )
continue;
if ( !strcmp( "spawn_purgatory", token ) )
{
info.m_type = PURGATORY_ENTER;
}
else if ( !strcmp( "spawn_loot", token ) )
{
info.m_type = LOOT_ISLAND_ENTER;
}
else
{
fprintf( errorFP, "ERROR @ Line %d: Unknown purgatory teleport '%s'\n", line, token );
continue;
}
}
else if ( !strcmp( "eyeball_stunned", token ) )
{
// eyeball_stunned with "short_stop" (attacker_position "-1951 -166 256")
info.m_type = EYEBALL_STUN;
// eat 'with'
token = strtok( NULL, " " );
token = strtok( NULL, "\" " );
if ( !token )
continue;
strcpy( info.m_weaponName, token );
// eat (attacker_position
token = strtok( NULL, " " );
token = strtok( NULL, "\" " );
if ( !token )
continue;
info.m_attackerPosX = atoi( token );
token = strtok( NULL, "\" " );
if ( !token )
continue;
info.m_attackerPosY = atoi( token );
token = strtok( NULL, "\" " );
if ( !token )
continue;
info.m_attackerPosZ = atoi( token );
}
else if ( !strcmp( "eyeball_killer", token ) )
{
// eyeball_killer with "short_stop" (attacker_position "-1213 271 236")
info.m_type = EYEBALL_KILLER;
// eat 'with'
token = strtok( NULL, " " );
token = strtok( NULL, "\" " );
if ( !token )
continue;
strcpy( info.m_weaponName, token );
// eat (attacker_position
token = strtok( NULL, " " );
token = strtok( NULL, "\" " );
if ( !token )
continue;
info.m_attackerPosX = atoi( token );
token = strtok( NULL, "\" " );
if ( !token )
continue;
info.m_attackerPosY = atoi( token );
token = strtok( NULL, "\" " );
if ( !token )
continue;
info.m_attackerPosZ = atoi( token );
}
else
{
fprintf( errorFP, "ERROR at Line %d: Unknown player event '%s'\n", line, token );
}
}
}
else
{
fprintf( errorFP, "ERROR at Line %d: Unknown data '%s'\n", line, token );
continue;
}
TheData.push_back( info );
}
fclose( fp );
//----------------------------------------
unsigned int bossSpawnCount = 0;
unsigned int bossEscapedCount = 0;
unsigned int bossDeathCount = 0;
unsigned int bossSpawnTimestamp = 0;
unsigned int bossDroppedCount = 0;
std::vector< int > bossLifetime;
std::vector< int > bossEscapeLifetime;
std::vector< int > bossDeathLifetime;
std::vector< int > bossStunCount;
bool isBossAlive = false;
for( unsigned int i=0; i<TheData.size(); ++i )
{
switch( TheData[i].m_type )
{
case EYEBALL_SPAWN:
if ( isBossAlive )
{
// we didn't get an ESCAPE or DEATH - map change?
// timestamp can go backwards on map change, so just don't count these lifetimes
bossLifetime.pop_back();
bossEscapeLifetime.pop_back();
bossDeathLifetime.pop_back();
bossStunCount.pop_back();
--bossSpawnCount;
++bossDroppedCount;
}
isBossAlive = true;
bossSpawnTimestamp = TheData[i].m_timestamp;
++bossSpawnCount;
bossLifetime.push_back(0);
bossEscapeLifetime.push_back(0);
bossDeathLifetime.push_back(0);
bossStunCount.push_back(0);
break;
case EYEBALL_ESCAPE:
if ( !isBossAlive )
{
fprintf( errorFP, "%d: Got ESCAPE when not spawned\n", i );
}
else
{
isBossAlive = false;
bossLifetime[ bossSpawnCount-1 ] = TheData[i].m_timestamp - bossSpawnTimestamp;
bossEscapeLifetime[ bossSpawnCount-1 ] = TheData[i].m_timestamp - bossSpawnTimestamp;
++bossEscapedCount;
}
break;
case EYEBALL_DEATH:
if ( !isBossAlive )
{
int lifetime = TheData[i].m_timestamp - bossSpawnTimestamp;
// using large time delta to account for stuns while escaping followed by a death
if ( lifetime - bossEscapeLifetime[ bossSpawnCount-1 ] < 60.0f )
{
// killed while escaping - he didn't actually escape!
bossEscapeLifetime[ bossSpawnCount-1 ] = 0;
--bossEscapedCount;
}
else
{
fprintf( errorFP, "%d: Got DEATH when not spawned\n", i );
}
}
else
{
isBossAlive = false;
bossLifetime[ bossSpawnCount-1 ] = TheData[i].m_timestamp - bossSpawnTimestamp;
bossDeathLifetime[ bossSpawnCount-1 ] = TheData[i].m_timestamp - bossSpawnTimestamp;
++bossDeathCount;
}
break;
case EYEBALL_STUN:
// skip alive check to collect stuns that happen just after escape starts
bossStunCount[ bossSpawnCount-1 ] = bossStunCount[ bossSpawnCount-1 ] + 1;
break;
case PURGATORY_ENTER:
break;
case PURGATORY_ESCAPE:
break;
case LOOT_ISLAND_ENTER:
break;
case EYEBALL_KILLER:
break;
}
}
fclose( errorFP );
//----------------------------------------
sprintf( buffer, "%s_cooked.txt", filename );
fp = fopen( buffer, "w" );
if ( !fp )
{
printf( "ERROR: Can't open output file '%s'\n", buffer );
return false;
}
if ( bossSpawnCount )
{
int minTime = 99999, maxTime = 0, avgTime = 0;
#define USE_COLUMNS
#ifdef USE_COLUMNS
fprintf( fp, "Lifetime, EscapeLifetime, DeathLifetime, StunCount\n" );
for( unsigned int i=0; i<bossSpawnCount; ++i )
{
fprintf( fp, "%d, %d, %d, %d\n", bossLifetime[i], bossEscapeLifetime[i], bossDeathLifetime[i], bossStunCount[i] );
if ( bossLifetime[i] < minTime )
{
minTime = bossLifetime[i];
}
if ( bossLifetime[i] > maxTime )
{
maxTime = bossLifetime[i];
}
avgTime += bossLifetime[i];
}
fprintf( fp, "\n" );
#else
fprintf( fp, "Lifetime, " );
for( unsigned int i=0; i<bossSpawnCount; ++i )
{
fprintf( fp, "%d, ", bossLifetime[i] );
if ( bossLifetime[i] < minTime )
{
minTime = bossLifetime[i];
}
if ( bossLifetime[i] > maxTime )
{
maxTime = bossLifetime[i];
}
avgTime += bossLifetime[i];
}
fprintf( fp, "\n" );
fprintf( fp, "EscapeLifetime, " );
for( unsigned int i=0; i<bossSpawnCount; ++i )
{
if ( bossEscapeLifetime[i] > 0 )
fprintf( fp, "%d, ", bossEscapeLifetime[i] );
}
fprintf( fp, "\n" );
fprintf( fp, "DeathLifetime, " );
for( unsigned int i=0; i<bossSpawnCount; ++i )
{
if ( bossDeathLifetime[i] > 0 )
fprintf( fp, "%d, ", bossDeathLifetime[i] );
}
fprintf( fp, "\n" );
fprintf( fp, "StunCount, " );
for( unsigned int i=0; i<bossSpawnCount; ++i )
{
fprintf( fp, "%d, ", bossStunCount[i] );
}
fprintf( fp, "\n\n" );
#endif
fprintf( fp, "Boss spawn count = %d\n", bossSpawnCount );
fprintf( fp, "Boss escape count = %d\n", bossEscapedCount );
fprintf( fp, "Boss escape ratio = %d%%\n", 100 * bossEscapedCount / bossSpawnCount );
fprintf( fp, "Boss killed count = %d\n", bossDeathCount );
fprintf( fp, "Boss killed ratio = %d%%\n", 100 * bossDeathCount / bossSpawnCount );
fprintf( fp, "Boss dropped count = %d\n", bossDroppedCount );
fprintf( fp, "Boss dropped ratio = %d%%\n", 100 * bossDroppedCount / bossSpawnCount );
fprintf( fp, "Boss MinLifetime %d, AvgLifeTime %d, MaxLifetime %d\n", minTime, avgTime / bossSpawnCount, maxTime );
}
else
{
fprintf( fp, "No Halloween data found.\n" );
}
fclose( fp );
return true;
}
//----------------------------------------------------------------------------------
int _tmain(int argc, _TCHAR* argv[])
{
if ( argc < 2 )
{
printf( "USAGE: %s <server log .txt file>\n", argv[0] );
return -1;
}
for( int i=1; i<argc; ++i )
{
ProcessData( argv[i] );
}
return 0;
}