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.
723 lines
15 KiB
723 lines
15 KiB
//========= 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; |
|
} |