Index: src/game/g_local.h =================================================================== --- src/game/g_local.h (revision 964) +++ src/game/g_local.h (working copy) @@ -518,6 +518,24 @@ TW_PASSED } timeWarning_t; +typedef enum +{ + BF_BUILT, + BF_DECONNED, + BF_DESTROYED +} buildableFate_t; + +typedef struct buildHistory_s buildHistory_t; +struct buildHistory_s +{ + gentity_t *ent; // who, NULL if they've disconnected (or aren't an ent) + char name[ MAX_NETNAME ]; // who, saves name if ent is NULL + int buildable; // what + buildableFate_t fate; // was it built, destroyed or deconned + buildHistory_t *next; // next oldest change + buildHistory_t *marked; // linked list of markdecon buildings taken +}; + // // this structure is cleared as each map is entered // @@ -656,6 +674,8 @@ char layout[ MAX_QPATH ]; pTeam_t surrenderTeam; + + buildHistory_t *buildHistory; } level_locals_t; #define CMD_CHEAT 0x01 @@ -762,6 +782,8 @@ void G_LayoutSelect( void ); void G_LayoutLoad( void ); void G_BaseSelfDestruct( pTeam_t team ); +int G_LogBuild( buildHistory_t *new ); +int G_CountBuildLog( void ); // // g_utils.c @@ -1184,6 +1206,8 @@ extern vmCvar_t g_privateMessages; +extern vmCvar_t g_buildLogMaxLength; + void trap_Printf( const char *fmt ); void trap_Error( const char *fmt ); int trap_Milliseconds( void ); Index: src/game/g_buildable.c =================================================================== --- src/game/g_buildable.c (revision 964) +++ src/game/g_buildable.c (working copy) @@ -618,6 +618,18 @@ */ void ASpawn_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) { + buildHistory_t *new; + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ent = ( attacker && attacker->client ) ? attacker : NULL; + if( new->ent ) + new->name[0] = 0; + else + Q_strncpyz( new->name, "", 8 ); + new->buildable = self->s.modelindex; + new->fate = BF_DESTROYED; + new->next = NULL; + G_LogBuild( new ); + G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue ); G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); @@ -870,6 +882,18 @@ */ void ABarricade_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) { + buildHistory_t *new; + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ent = ( attacker && attacker->client ) ? attacker : NULL; + if( new->ent ) + new->name[0] = 0; + else + Q_strncpyz( new->name, "", 8 ); + new->buildable = self->s.modelindex; + new->fate = BF_DESTROYED; + new->next = NULL; + G_LogBuild( new ); + G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue ); G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); @@ -1276,6 +1300,18 @@ void AHovel_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) { vec3_t dir; + buildHistory_t *new; + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ent = ( attacker && attacker->client ) ? attacker : NULL; + if( new->ent ) + new->name[0] = 0; + else + Q_strncpyz( new->name, "", 8 ); + new->buildable = self->s.modelindex; + new->fate = BF_DESTROYED; + new->next = NULL; + G_LogBuild( new ); + VectorCopy( self->s.origin2, dir ); @@ -2269,6 +2305,18 @@ */ void HSpawn_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) { + buildHistory_t *new; + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ent = ( attacker && attacker->client ) ? attacker : NULL; + if( new->ent ) + new->name[0] = 0; + else + Q_strncpyz( new->name, "", 8 ); + new->buildable = self->s.modelindex; + new->fate = BF_DESTROYED; + new->next = NULL; + G_LogBuild( new ); + //pretty events and cleanup G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue ); G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); @@ -2600,6 +2648,8 @@ { int i; gentity_t *ent; + buildHistory_t *new, *last; + last = level.buildHistory; if( !g_markDeconstruct.integer ) return; // Not enabled, can't deconstruct anything @@ -2608,6 +2658,16 @@ { ent = level.markedBuildables[ i ]; + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ent = NULL; + Q_strncpyz( new->name, "", 12 ); + new->buildable = ent->s.modelindex; + new->fate = BF_DECONNED; + new->next = NULL; + new->marked = NULL; + + last = last->marked = new; + G_FreeEntity( ent ); } } @@ -3027,7 +3087,15 @@ static gentity_t *G_Build( gentity_t *builder, buildable_t buildable, vec3_t origin, vec3_t angles ) { gentity_t *built; + buildHistory_t *new; vec3_t normal; + + // initialise the buildhistory so other functions can use it + if( builder && builder->client ) + { + new = G_Alloc( sizeof( buildHistory_t ) ); + G_LogBuild( new ); + } // Free existing buildables G_FreeMarkedBuildables( ); @@ -3260,6 +3328,16 @@ trap_LinkEntity( built ); + // ok we're all done building, so what we log here should be the final values + if( builder && builder->client ) // log ingame building only + { + new = level.buildHistory; + new->ent = builder; + new->name[0] = 0; + new->buildable = buildable; + new->fate = BF_BUILT; + } + return built; } @@ -3719,3 +3797,38 @@ } } +int G_LogBuild( buildHistory_t *new ) +{ + new->next = level.buildHistory; + level.buildHistory = new; + return G_CountBuildLog(); +} + +int G_CountBuildLog( void ) +{ + buildHistory_t *ptr, *mark; + int i = 0, overflow; + for( ptr = level.buildHistory; ptr; ptr = ptr->next, i++ ); + if( i > g_buildLogMaxLength.integer ) + { + for( overflow = i - g_buildLogMaxLength.integer; overflow > 0; overflow-- ) + { + ptr = level.buildHistory; + while( ptr->next ) + { + if( ptr->next->next ) + ptr = ptr->next; + else + { + while( mark = ptr->next ) + { + ptr->next = ptr->next->marked; + G_Free( mark ); + } + } + } + } + return g_buildLogMaxLength.integer; + } + return i; +} Index: src/game/g_main.c =================================================================== --- src/game/g_main.c (revision 964) +++ src/game/g_main.c (working copy) @@ -131,6 +131,8 @@ vmCvar_t g_privateMessages; +vmCvar_t g_buildLogMaxLength; + vmCvar_t g_tag; static cvarTable_t gameCvarTable[ ] = @@ -248,6 +250,8 @@ { &g_adminTempBan, "g_adminTempBan", "120", CVAR_ARCHIVE, 0, qfalse }, { &g_privateMessages, "g_privateMessages", "1", CVAR_ARCHIVE, 0, qfalse }, + + { &g_buildLogMaxLength, "g_buildLogMaxLength", "25", CVAR_ARCHIVE, 0, qfalse }, { &g_tag, "g_tag", "main", CVAR_INIT, 0, qfalse }, @@ -1523,7 +1527,18 @@ { int i; gclient_t *cl; + buildHistory_t *tmp, *mark; + while( tmp = level.buildHistory ) + { + level.buildHistory = level.buildHistory->next; + while( mark = tmp ) + { + tmp = tmp->marked; + G_Free( mark ); + } + } + if( G_MapRotationActive( ) ) G_AdvanceMapRotation( ); else Index: src/game/g_admin.c =================================================================== --- src/game/g_admin.c (revision 964) +++ src/game/g_admin.c (working copy) @@ -60,6 +60,12 @@ "[^3name|slot#|IP^7] (^5time^7) (^5reason^7)" }, + {"buildlog", G_admin_buildlog, "U", + "display a list of recent builds and deconstructs, optionally specifying" + " a team", + "(^5starting log index^7)(^5a|h^7)", + }, + {"cancelvote", G_admin_cancelvote, "c", "cancel a vote taking place", "" @@ -2962,6 +2968,158 @@ return qtrue; } +qboolean G_admin_buildlog( gentity_t *ent, int skiparg ) +{ +#define LOG_DISPLAY_LENGTH 10 + buildHistory_t *ptr; + int start, i, len; + pTeam_t team; + char startbuf[ 12 ], message[ MAX_STRING_CHARS ], *teamchar; + char *name, *action, *buildablename, markstring[ MAX_STRING_CHARS ]; + if( !g_buildLogMaxLength.integer ) + { + ADMP( "^3!buildlog: ^7build logging is disabled" ); + return qfalse; + } + if( G_SayArgc() >= 2 + skiparg ) + { + G_SayArgv( 1 + skiparg, startbuf, sizeof( startbuf ) ); + // parse a repeat value (this is atoi except startbuf[i] is the end of the + // number, so the rest of the string can be parsed) + for( i = 0; startbuf[i] > '0' && startbuf[i] < '9'; i++ ) + { + start *= 10; + start += startbuf[i] - '0'; + } + // grab a team + switch( startbuf[i] ) + { + case 'A': + case 'a': + team = PTE_ALIENS; + break; + case 'H': + case 'h': + team = PTE_HUMANS; + break; + default: + team = PTE_NONE; + } + } + else + start = 0; + // !buildlog can be abused, so let everyone know when it is used + AP( va( "print \"^3!buildlog: ^7%s^7 requested a log of recent building" + " activity\n\"", + ( ent ) ? ent->client->pers.netname : "console" ) ); + len = G_CountBuildLog(); // also clips the log if too long + if( !len ) + { + ADMP( "^3!buildlog: ^7no build log found\n" ); + return qfalse; + } + *message = 0; + // ensure start is a useful value + if( start > len - LOG_DISPLAY_LENGTH ) + start = len - LOG_DISPLAY_LENGTH; + // skip to start entry + for( ptr = level.buildHistory, i = len; i > len - start; + i--, ptr = ptr->next ); + for( ; i + LOG_DISPLAY_LENGTH > len - start && i > 0; i--, ptr = ptr->next ) + { + if( !ptr ) break; // run out of log + *markstring = 0; // reinit markstring + // check team + if( team != PTE_NONE && team != BG_FindTeamForBuildable( ptr->buildable ) ) + { + start++; // loop an extra time because we skipped one + continue; + } + // set name to the ent's current name or last recorded name + if( ptr->ent ) + { + if( ptr->ent->client ) + name = ptr->ent->client->pers.netname; + else + name = ""; // any non-client action + } + else + name = ptr->name; + switch( ptr->fate ) + { + case BF_BUILT: + action = "built a"; + break; + case BF_DECONNED: + action = "deconstructed a"; + break; + case BF_DESTROYED: + action = "destroyed a"; + break; + default: + action = "\0"; // erm + break; + } + // handle buildables removed by markdecon + if( ptr->marked ) + { + buildHistory_t *mark; + int j, markdecon[ BA_NUM_BUILDABLES ], and = 2; + char bnames[32], *article; + mark = ptr; + // count the number of buildables + memset( markdecon, 0, sizeof( markdecon ) ); + while( (mark = mark->marked) ) + markdecon[ mark->buildable ]++; + // reverse order makes grammar easier + for( j = BA_NUM_BUILDABLES; j >= 0; j-- ) + { + buildablename = BG_FindHumanNameForBuildable( j ); + // plural is easy + if( markdecon[ j ] > 1 ) + Com_sprintf( bnames, 32, "%d %ss", markdecon[ j ], buildablename ); + // use an appropriate article + else if( markdecon[ j ] == 1 ) + { + if( BG_FindUniqueTestForBuildable( j ) ) + article = "the"; // if only one + else if( strchr( "aeiouAEIOU", *buildablename ) ) + article = "an"; // if first char is vowel + else + article = "a"; + Com_sprintf( bnames, 32, "%s %s", article, buildablename ); + } + else + continue; // none of this buildable + // C grammar: x, y, and z + // the integer and is 2 initially, the test means it is used on the + // second sprintf only, the reverse order makes this second to last + // the comma is only printed if there is already some markstring i.e. + // not the first time (which would put it on the end of the string) + Com_sprintf( markstring, sizeof( markstring ), "%s%s %s%s", bnames, + ( *markstring ) ? "," : "", + ( and-- == 1 ) ? "and " : "", markstring ); + } + } + buildablename = BG_FindHumanNameForBuildable( ptr->buildable ); + switch( BG_FindTeamForBuildable( ptr->buildable ) ) + { + case PTE_ALIENS: teamchar = "^1A"; break; + case PTE_HUMANS: teamchar = "^4H"; break; + default: teamchar = " "; // space so it lines up neatly + } + // prepend the information to the string as we go back in buildhistory + // so the earliest events are at the top + Com_sprintf( message, MAX_STRING_CHARS, "%3d %s^7 %s^7 %s%s %s%s%s\n%s", i, + teamchar, name, action, + ( strchr( "aeiouAEIOU", buildablename[0] ) ) ? "n" : "", + buildablename, ( markstring[0] ) ? ", removing " : "", + markstring, message ); + } + ADMP( va( "%s", message ) ); + return qtrue; +} + /* ================ G_admin_print Index: src/game/g_admin.h =================================================================== --- src/game/g_admin.h (revision 964) +++ src/game/g_admin.h (working copy) @@ -167,6 +167,7 @@ qboolean G_admin_namelog( gentity_t *ent, int skiparg ); qboolean G_admin_lock( gentity_t *ent, int skiparg ); qboolean G_admin_unlock( gentity_t *ent, int skiparg ); +qboolean G_admin_buildlog( gentity_t *ent, int skiparg ); void G_admin_print( gentity_t *ent, char *m ); void G_admin_buffer_print( gentity_t *ent, char *m ); Index: src/game/g_client.c =================================================================== --- src/game/g_client.c (revision 964) +++ src/game/g_client.c (working copy) @@ -1654,12 +1654,23 @@ gentity_t *ent; gentity_t *tent; int i; + buildHistory_t *ptr; ent = g_entities + clientNum; if( !ent->client ) return; + // look through the bhist and readjust it if the referenced ent has left + for( ptr = level.buildHistory; ptr; ptr = ptr->next ) + { + if( ptr->ent == ent ) + { + ptr->ent = NULL; + Q_strncpyz( ptr->name, ent->client->pers.netname, MAX_NETNAME ); + } + } + G_admin_namelog_update( ent->client, qtrue ); G_LeaveTeam( ent ); Index: src/game/g_cmds.c =================================================================== --- src/game/g_cmds.c (revision 964) +++ src/game/g_cmds.c (working copy) @@ -1845,6 +1845,17 @@ } else { + buildHistory_t *new; + + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ent = ent; + new->name[0] = 0; + new->buildable = traceEnt->s.modelindex; + new->fate = BF_DECONNED; + new->next = NULL; + new->marked = NULL; + G_LogBuild( new ); + G_TeamCommand( ent->client->pers.teamSelection, va( "print \"%s ^3DECONSTRUCTED^7 by %s^7\n\"", BG_FindHumanNameForBuildable( traceEnt->s.modelindex ),