Index: src/game/g_local.h =================================================================== --- src/game/g_local.h (revision 964) +++ src/game/g_local.h (working copy) @@ -518,6 +518,30 @@ TW_PASSED } timeWarning_t; +typedef enum +{ + BF_BUILT, + BF_DECONNED, + BF_DESTROYED +} buildableFate_t; + +// record all changes to the buildable layout - build, decon, destroy - and +// enough information to revert that change +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 + vec3_t origin; // where + vec3_t angles; // which way round + vec3_t origin2; // I don't know what the hell these are, but layoutsave saves + vec3_t angles2; // them so I will do the same + 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 +680,8 @@ char layout[ MAX_QPATH ]; pTeam_t surrenderTeam; + + buildHistory_t *buildHistory; } level_locals_t; #define CMD_CHEAT 0x01 @@ -762,6 +788,13 @@ void G_LayoutSelect( void ); void G_LayoutLoad( void ); void G_BaseSelfDestruct( pTeam_t team ); +gentity_t *G_InstantBuild( buildable_t buildable, vec3_t origin, + vec3_t angles, vec3_t origin2, vec3_t angles2 ); +void G_SpawnRevertedBuildable( buildHistory_t *bh, qboolean mark ); +void G_CommitRevertedBuildable( gentity_t *ent ); +qboolean G_RevertCanFit( buildHistory_t *bh ); +int G_LogBuild( buildHistory_t *new ); +int G_CountBuildLog( void ); // // g_utils.c @@ -1184,6 +1217,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,22 @@ */ 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; + VectorCopy( self->s.pos.trBase, new->origin ); + VectorCopy( self->s.angles, new->angles ); + VectorCopy( self->s.origin2, new->origin2 ); + VectorCopy( self->s.angles2, new->angles2 ); + new->fate = BF_DESTROYED; + new->next = NULL; + G_LogBuild( new ); + G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue ); G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); @@ -870,6 +886,22 @@ */ 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; + VectorCopy( self->s.pos.trBase, new->origin ); + VectorCopy( self->s.angles, new->angles ); + VectorCopy( self->s.origin2, new->origin2 ); + VectorCopy( self->s.angles2, new->angles2 ); + new->fate = BF_DESTROYED; + new->next = NULL; + G_LogBuild( new ); + G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue ); G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); @@ -1276,6 +1308,22 @@ 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; + VectorCopy( self->s.pos.trBase, new->origin ); + VectorCopy( self->s.angles, new->angles ); + VectorCopy( self->s.origin2, new->origin2 ); + VectorCopy( self->s.angles2, new->angles2 ); + new->fate = BF_DESTROYED; + new->next = NULL; + G_LogBuild( new ); + VectorCopy( self->s.origin2, dir ); @@ -2269,6 +2317,22 @@ */ 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; + VectorCopy( self->s.pos.trBase, new->origin ); + VectorCopy( self->s.angles, new->angles ); + VectorCopy( self->s.origin2, new->origin2 ); + VectorCopy( self->s.angles2, new->angles2 ); + 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 +2664,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 +2674,20 @@ { ent = level.markedBuildables[ i ]; + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ent = NULL; + Q_strncpyz( new->name, "", 12 ); + new->buildable = ent->s.modelindex; + VectorCopy( ent->s.pos.trBase, new->origin ); + VectorCopy( ent->s.angles, new->angles ); + VectorCopy( ent->s.origin2, new->origin2 ); + VectorCopy( ent->s.angles2, new->angles2 ); + new->fate = BF_DECONNED; + new->next = NULL; + new->marked = NULL; + + last = last->marked = new; + G_FreeEntity( ent ); } } @@ -3027,7 +3107,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 +3348,20 @@ 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; + VectorCopy( built->s.pos.trBase, new->origin ); + VectorCopy( built->s.angles, new->angles ); + VectorCopy( built->s.origin2, new->origin2 ); + VectorCopy( built->s.angles2, new->angles2 ); + new->fate = BF_BUILT; + } + return built; } @@ -3634,6 +3736,207 @@ /* ============ +G_InstantBuild + +This function is extremely similar to the few functions that place a +buildable on map load. It exists because G_LayoutBuildItem takes a couple +of frames to finish spawning it, so it's not truly instant +Do not call this function immediately after the map loads - that's what +G_LayoutBuildItem is for. +============ +*/ +gentity_t *G_InstantBuild( buildable_t buildable, vec3_t origin, vec3_t angles, + vec3_t origin2, vec3_t angles2 ) +{ + gentity_t *builder, *built; + trace_t tr; + vec3_t dest; + + builder = G_Spawn( ); + builder->client = 0; + VectorCopy( origin, builder->s.pos.trBase ); + VectorCopy( angles, builder->s.angles ); + VectorCopy( origin2, builder->s.origin2 ); + VectorCopy( angles2, builder->s.angles2 ); +//old method didn't quite work out +//builder->s.modelindex = buildable; +//G_FinishSpawningBuildable( builder ); + + built = G_Build( builder, buildable, + builder->s.pos.trBase, builder->s.angles ); + G_FreeEntity( builder ); + + built->takedamage = qtrue; + built->spawned = qtrue; //map entities are already spawned + built->health = BG_FindHealthForBuildable( buildable ); + built->s.generic1 |= B_SPAWNED_TOGGLEBIT; + + // drop towards normal surface + VectorScale( built->s.origin2, -4096.0f, dest ); + VectorAdd( dest, built->s.origin, dest ); + + trap_Trace( &tr, built->s.origin, built->r.mins, built->r.maxs, dest, + built->s.number, built->clipmask ); + if( tr.startsolid ) + { + G_Printf( S_COLOR_YELLOW "G_FinishSpawningBuildable: %s startsolid at %s\n", + built->classname, vtos( built->s.origin ) ); + G_FreeEntity( built ); + return NULL; + } + + //point items in the correct direction + VectorCopy( tr.plane.normal, built->s.origin2 ); + + // allow to ride movers + built->s.groundEntityNum = tr.entityNum; + + G_SetOrigin( built, tr.endpos ); + + trap_LinkEntity( built ); + return built; +} + +/* +============ +G_SpawnRevertedBuildable + +Given a buildhistory, try to replace the lost buildable +============ +*/ +void G_SpawnRevertedBuildable( buildHistory_t *bh, qboolean mark ) +{ + vec3_t mins, maxs; + int i, j, blockCount, blockers[ MAX_GENTITIES ], toRecontent[ MAX_GENTITIES ]; + gentity_t *targ, *built; + + BG_FindBBoxForBuildable( bh->buildable, mins, maxs ); + VectorAdd( bh->origin, mins, mins ); + VectorAdd( bh->origin, maxs, maxs ); + blockCount = trap_EntitiesInBox( mins, maxs, blockers, MAX_GENTITIES ); + for( i = j = 0; i < blockCount; i++ ) + { + targ = g_entities + blockers[ i ]; + if( targ->s.eType == ET_BUILDABLE ) + G_FreeEntity( targ ); + else if( targ->s.eType == ET_PLAYER ) + { + targ->r.contents = 0; // make it intangible + toRecontent[ j++ ] = targ-g_entities; // and remember it + } + } + level.numBuildablesForRemoval = 0; + built = G_InstantBuild( bh->buildable, bh->origin, bh->angles, + bh->origin2, bh->angles2 ); + if( built ) + { + built->r.contents = 0; + built->think = G_CommitRevertedBuildable; + built->nextthink = level.time + 50; + built->deconstruct = mark; + } + for( i = 0; i < j; i++ ) + g_entities[ toRecontent[ i ] ].r.contents = CONTENTS_BODY; +} + +/* +============ +G_CommitRevertedBuildable + +Check if there's anyone occupying me, and if not, become solid and operate as +normal. Else, try to get rid of them. +============ +*/ +void G_CommitRevertedBuildable( gentity_t *ent ) +{ + gentity_t *targ; + int i, n, occupants[ MAX_GENTITIES ]; + vec3_t mins, maxs; + VectorAdd( ent->s.origin, ent->r.mins, mins ); + VectorAdd( ent->s.origin, ent->r.maxs, maxs ); + trap_UnlinkEntity( ent ); + n = trap_EntitiesInBox( mins, maxs, occupants, MAX_GENTITIES ); + trap_LinkEntity( ent ); + if( n == 0 ) + { // we're in the clear! + ent->r.contents = MASK_PLAYERSOLID; + trap_LinkEntity( ent ); // relink + // oh dear, manual think set + switch( ent->s.modelindex ) + { + case BA_A_SPAWN: ent->think = ASpawn_Think; break; + case BA_A_BARRICADE: + case BA_A_BOOSTER: ent->think = ABarricade_Think; break; + case BA_A_ACIDTUBE: ent->think = AAcidTube_Think; break; + case BA_A_HIVE: ent->think = AHive_Think; break; + case BA_A_TRAPPER: ent->think = ATrapper_Think; break; + case BA_A_OVERMIND: ent->think = AOvermind_Think; break; + case BA_A_HOVEL: ent->think = AHovel_Think; break; + case BA_H_SPAWN: ent->think = HSpawn_Think; break; + case BA_H_MGTURRET: ent->think = HMGTurret_Think; break; + case BA_H_TESLAGEN: ent->think = HTeslaGen_Think; break; + case BA_H_ARMOURY: ent->think = HArmoury_Think; break; + case BA_H_DCC: ent->think = HDCC_Think; break; + case BA_H_MEDISTAT: ent->think = HMedistat_Think; break; + case BA_H_REACTOR: ent->think = HReactor_Think; break; + case BA_H_REPEATER: ent->think = HRepeater_Think; break; + } + ent->nextthink = + level.time + BG_FindNextThinkForBuildable( ent->s.modelindex ); + // oh if only everything was that simple + return; + } + for( i = 0; i < n; i++ ) + { + vec3_t gtfo; + targ = g_entities + occupants[ i ]; + if( targ->client ) + { + VectorSet( gtfo, crandom() * 150, crandom() * 150, random() * 150 ); + VectorAdd( targ->client->ps.velocity, gtfo, targ->client->ps.velocity ); + } + } + ent->nextthink = level.time + 50; +} + +/* +============ +G_RevertCanFit + +take a bhist and make sure you're not overwriting anything by placing it +============ +*/ +qboolean G_RevertCanFit( buildHistory_t *bh ) +{ + int i, num, blockers[ MAX_GENTITIES ]; + vec3_t mins, maxs; + gentity_t *targ; + vec3_t dist; + + BG_FindBBoxForBuildable( bh->buildable, mins, maxs ); + VectorAdd( bh->origin, mins, mins ); + VectorAdd( bh->origin, maxs, maxs ); + num = trap_EntitiesInBox( mins, maxs, blockers, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + targ = g_entities + blockers[ i ]; + if( targ->s.eType == ET_BUILDABLE ) + { + VectorSubtract( bh->origin, targ->s.pos.trBase, dist ); + if( targ->s.modelindex == bh->buildable && VectorLength( dist ) < 10 && + targ->health <= 0 ) + continue; // it's the same buildable, hasn't blown up yet + else + return qfalse; // can't get rid of this one + } + else + continue; + } + return qtrue; +} + +/* +============ G_LayoutLoad load the layout .dat file indicated by level.layout and spawn buildables @@ -3719,3 +4022,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", "" @@ -145,6 +151,11 @@ "(^5layout^7)" }, + {"revert", G_admin_revert, "R", + "revert one or more buildlog events, optionally of only one team", + "(^5number^7)(^5a|h^7)" + }, + {"setlevel", G_admin_setlevel, "s", "sets the admin level of a player", "[^3name|slot#|admin#^7] [^3level^7]" @@ -2962,6 +2973,361 @@ 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; +} + +qboolean G_admin_revert( gentity_t *ent, int skiparg ) +{ + int i, j = 0, repeat, len; + pTeam_t team; + qboolean force = qfalse; + gentity_t *targ; + buildHistory_t *ptr, *tmp, *mark, *prev; + vec3_t dist; + char rbuf[ 8 ], *name, *bname, *action, *article, *teamchar; + len = G_CountBuildLog(); + if( !len ) + { + ADMP( "^3!revert: ^7no build log found\n" ); + return qfalse; + } + if( G_SayArgc() >= 2 + skiparg ) + { + G_SayArgv( 1 + skiparg, rbuf, sizeof( rbuf ) ); + // this instead of atoi so rbuf[i] can be the end of the number + for( i = repeat = 0; rbuf[i] > '0' && rbuf[i] < '9'; i++ ) + { + repeat *= 10; + repeat += rbuf[i] - '0'; + } + // at least once + if( repeat == 0 ) repeat = 1; + // get a team or force indicator + switch( rbuf[i++] ) + { + case 'A': + case 'a': + team = PTE_ALIENS; + break; + case 'H': + case 'h': + team = PTE_HUMANS; + break; + case '!': + force = qtrue; // teamless force + default: + team = PTE_NONE; + } + if( rbuf[i] == '!' ) // team and force + force = qtrue; + } + else + { + // defaults + repeat = 1; + team = PTE_NONE; + } + // normalise repeat to sensible values + if( repeat > len ) repeat = len; + for( i = 0, ptr = prev = level.buildHistory; repeat > 0; repeat--, j = 0 ) + { + if( !ptr ) break; // run out of bhist + if( team != PTE_NONE && + team != BG_FindTeamForBuildable( ptr->buildable ) ) + { + // team doesn't match, so skip this ptr and reset prev + prev = ptr; + ptr = ptr->next; + // we don't want to count this one so counteract the decrement by the for + repeat++; + continue; + } + // get the ent's current or last recorded name + if( ptr->ent ) + { + if( ptr->ent->client ) + name = ptr->ent->client->pers.netname; + else + name = ""; // non-client actions + } + else + name = ptr->name; + bname = BG_FindHumanNameForBuildable( ptr->buildable ); + action = "\0"; + switch( ptr->fate ) + { + case BF_BUILT: + action = "build"; + for( j = MAX_CLIENTS, targ = g_entities + j; + j < level.num_entities; j++, targ++ ) + { + // easy checks first to save time + if( targ->s.eType != ET_BUILDABLE ) + continue; + if( targ->s.modelindex != ptr->buildable ) + continue; + VectorSubtract( targ->s.pos.trBase, + ptr->origin, dist ); + if( VectorLength( dist ) > 10 ) + continue; // 10 is somewhat arbitrary, watch for false pos/neg + // if we didn't continue then it's this one, unlink it but we can't + // free it yet, because the markdecon buildables might not place + trap_UnlinkEntity( targ ); + break; + } + // if there are marked buildables to replace, and we aren't overriding + // space check, check they can fit before acting + if( ptr->marked && !force ) + { + for( mark = ptr->marked; mark; mark = mark->marked ) + if( !G_RevertCanFit( mark ) ) + { + trap_LinkEntity( targ ); // put it back, we failed + switch( team ) + { + case PTE_ALIENS: teamchar = "a"; break; + case PTE_HUMANS: teamchar = "h"; break; + default: teamchar = ""; // this is unlikely to ever occur + } + // you only need to specify repeat if it isn't one, reflect this + if( repeat > 1 ) + Com_sprintf( rbuf, sizeof( rbuf ), "%d", repeat ); + else + *rbuf = 0; + // note that repeat has already been decremented by any + // successful loops, so the suggested command will be + // adjusted appropriately + ADMP( va( "^3!revert: ^7revert aborted: reverting this %s would " + "conflict with another buildable, use ^3!revert %s%s! ^7to " + "override\n", action, rbuf, teamchar ) ); + return qfalse; + } + } + // if we haven't returned yet then we're good to go, free it + G_FreeEntity( targ ); + // put the marked buildables back and mark them again + if( ptr->marked ) // there may be a more efficient way of doing this + { + for( mark = ptr->marked; mark; mark = mark->marked ) + G_SpawnRevertedBuildable( mark, qtrue ); + } + break; + case BF_DECONNED: + action = "deconstruction"; + case BF_DESTROYED: + if( !*action ) action = "destruction"; + // if we're not overriding and the replacement can't fit, as before + if( !force && !G_RevertCanFit( ptr ) ) + { + switch( team ) + { + case PTE_ALIENS: teamchar = "A"; break; + case PTE_HUMANS: teamchar = "H"; break; + default: teamchar = ""; + } + if( repeat > 1 ) + Com_sprintf( rbuf, sizeof( rbuf ), "%d", repeat ); + else + Q_strncpyz( rbuf, "", 1 ); + ADMP( va( "^3!revert: ^7revert aborted: reverting this %s would " + "conflict with another buildable, use ^3!revert %s%s! ^7to " + "override\n", action, rbuf, teamchar ) ); + return qfalse; + } + // else replace it but don't mark it (it might have been marked before + // but it isn't that important) + G_SpawnRevertedBuildable( ptr, qfalse ); + break; + default: + // if this happens something has gone wrong + ADMP( "^3!revert: ^7incomplete or corrupted build log entry\n" ); + /* quarantine and dispose of the log, it's dangerous + trap_Cvar_Set( "g_buildLogMaxLength", "0" ); + G_CountBuildLog(); + */ + return qfalse; + } + if( j == level.num_entities ) + // any instance of this is technically a bug but not a major one + // a better solution would be just cutting it out of the log and moving + // on silently, but ideally they shouldn't arise at all + ADMP( "^3!revert: ^7could not find logged buildable\n" ); + else + { + // this is similar to the buildlog stuff + if( BG_FindUniqueTestForBuildable( ptr->buildable ) ) + article = "the"; + else if( strchr( "aeiouAEIOU", *bname ) ) + article = "an"; + else + article = "a"; + AP( va( "print \"%s^7 reverted %s^7'%s %s of %s %s\n\"", + ( ent ) ? ent->client->pers.netname : "console", + name, strchr( "Ss", name[ strlen(name) - 1 ] ) ? "" : "s", + action, article, bname ) ); + // remove the reverted entry + // ptr moves on, prev just readjusts ->next unless it is about to be + // freed, in which case it is forced to move on too + tmp = ptr; + if( ptr == level.buildHistory ) + prev = level.buildHistory = ptr = ptr->next; + else + prev->next = ptr = ptr->next; + G_Free( tmp ); + } + } + 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,8 @@ 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 ); +qboolean G_admin_revert( 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,21 @@ } else { + buildHistory_t *new; + + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ent = ent; + new->name[0] = 0; + new->buildable = traceEnt->s.modelindex; + VectorCopy( traceEnt->s.pos.trBase, new->origin ); + VectorCopy( traceEnt->s.angles, new->angles ); + VectorCopy( traceEnt->s.origin2, new->origin2 ); + VectorCopy( traceEnt->s.angles2, new->angles2 ); + 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 ),