diff -Nur ioq3/code/cgame/cg_draw.c qlone-mod/code/cgame/cg_draw.c --- ioq3/code/cgame/cg_draw.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/cgame/cg_draw.c 2020-04-14 12:42:39.000000000 +0200 @@ -1638,7 +1638,10 @@ int color; float vscale; - if ( !cg_lagometer.integer || cgs.localServer ) { + //unlagged - misc + //if ( !cg_lagometer.integer || cgs.localServer ) { + if ( !cg_lagometer.integer ) { + //unlagged - misc CG_DrawDisconnect(); return; } diff -Nur ioq3/code/cgame/cg_ents.c qlone-mod/code/cgame/cg_ents.c --- ioq3/code/cgame/cg_ents.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/cgame/cg_ents.c 2020-04-15 10:14:20.000000000 +0200 @@ -795,6 +795,15 @@ */ static void CG_CalcEntityLerpPositions( centity_t *cent ) { + //unlagged - projectile nudge + // this will be set to how far forward projectiles will be extrapolated + int timeshift = 0; + //unlagged - projectile nudge + + //unlagged - smooth clients #2 + // this is done server-side now - cg_smoothClients is undefined + // players will always be TR_INTERPOLATE + /* // if this player does not want to see extrapolated players if ( !cg_smoothClients.integer ) { // make sure the clients use TR_INTERPOLATE @@ -803,6 +812,8 @@ cent->nextState.pos.trType = TR_INTERPOLATE; } } + */ + //unlagged - smooth clients #2 if ( cent->interpolate && cent->currentState.pos.trType == TR_INTERPOLATE ) { CG_InterpolateEntityPosition( cent ); @@ -812,14 +823,65 @@ // first see if we can interpolate between two snaps for // linear extrapolated clients if ( cent->interpolate && cent->currentState.pos.trType == TR_LINEAR_STOP && - cent->currentState.number < MAX_CLIENTS) { + cent->currentState.number < MAX_CLIENTS) { CG_InterpolateEntityPosition( cent ); return; } + //unlagged - timenudge extrapolation + // interpolating failed (probably no nextSnap), so extrapolate + // this can also happen if the teleport bit is flipped, but that + // won't be noticeable + if ( cent->currentState.number < MAX_CLIENTS && + cent->currentState.clientNum != cg.predictedPlayerState.clientNum ) { + cent->currentState.pos.trType = TR_LINEAR_STOP; + cent->currentState.pos.trTime = cg.snap->serverTime; + cent->currentState.pos.trDuration = 1000 / sv_fps.integer; + } + //unlagged - timenudge extrapolation + + //unlagged - projectile nudge + /* // just use the current frame and evaluate as best we can BG_EvaluateTrajectory( ¢->currentState.pos, cg.time, cent->lerpOrigin ); BG_EvaluateTrajectory( ¢->currentState.apos, cg.time, cent->lerpAngles ); + */ + // if it's a missile but not a grappling hook + if ( cent->currentState.eType == ET_MISSILE && cent->currentState.weapon != WP_GRAPPLING_HOOK ) { + // if it's one of ours + if ( cent->currentState.otherEntityNum == cg.clientNum ) { + // extrapolate one server frame's worth - this will correct for tiny + // visual inconsistencies introduced by backward-reconciling all players + // one server frame before running projectiles + timeshift = 1000 / sv_fps.integer; + } + // if it's not, and it's not a grenade launcher + else if ( cent->currentState.weapon != WP_GRENADE_LAUNCHER ) { + // extrapolate based on cg_projectileNudge + timeshift = cg_projectileNudge.integer + 1000 / sv_fps.integer; + } + } + + BG_EvaluateTrajectory( ¢->currentState.pos, cg.time + timeshift, cent->lerpOrigin ); + BG_EvaluateTrajectory( ¢->currentState.apos, cg.time + timeshift, cent->lerpAngles ); + + // if there's a time shift + if ( timeshift != 0 ) { + trace_t tr; + vec3_t lastOrigin; + + BG_EvaluateTrajectory( ¢->currentState.pos, cg.time, lastOrigin ); + + CG_Trace( &tr, lastOrigin, vec3_origin, vec3_origin, cent->lerpOrigin, cent->currentState.number, MASK_SHOT ); + + // don't let the projectile go through the floor + if ( tr.fraction < 1.0f ) { + cent->lerpOrigin[0] = lastOrigin[0] + tr.fraction * ( cent->lerpOrigin[0] - lastOrigin[0] ); + cent->lerpOrigin[1] = lastOrigin[1] + tr.fraction * ( cent->lerpOrigin[1] - lastOrigin[1] ); + cent->lerpOrigin[2] = lastOrigin[2] + tr.fraction * ( cent->lerpOrigin[2] - lastOrigin[2] ); + } + } + //unlagged - projectile nudge // adjust for riding a mover if it wasn't rolled into the predicted // player state @@ -1087,10 +1149,30 @@ // lerp the non-predicted value for lightning gun origins CG_CalcEntityLerpPositions( &cg_entities[ cg.snap->ps.clientNum ] ); + //unlagged - early transitioning + if ( cg.nextSnap ) { + // pre-add some of the entities sent over by the server + // we have data for them and they don't need to interpolate + for ( num = 0 ; num < cg.nextSnap->numEntities ; num++ ) { + cent = &cg_entities[ cg.nextSnap->entities[ num ].number ]; + if ( cent->nextState.eType == ET_MISSILE || cent->nextState.eType == ET_GENERAL ) { + // transition it immediately and add it + CG_TransitionEntity( cent ); + cent->interpolate = qtrue; + CG_AddCEntity( cent ); + } + } + } + //unlagged - early transitioning + // add each entity sent over by the server for ( num = 0 ; num < cg.snap->numEntities ; num++ ) { cent = &cg_entities[ cg.snap->entities[ num ].number ]; - CG_AddCEntity( cent ); + //unlagged - early transitioning + if ( !cg.nextSnap || ((cent->nextState.eType != ET_MISSILE) && (cent->nextState.eType != ET_GENERAL)) ) { + //unlagged - early transitioning + CG_AddCEntity( cent ); + } } } diff -Nur ioq3/code/cgame/cg_event.c qlone-mod/code/cgame/cg_event.c --- ioq3/code/cgame/cg_event.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/cgame/cg_event.c 2020-04-14 17:21:53.000000000 +0200 @@ -983,7 +983,8 @@ case EV_RAILTRAIL: DEBUGNAME("EV_RAILTRAIL"); cent->currentState.weapon = WP_RAILGUN; - + //unlagged - attack prediction #2 + /* if(es->clientNum == cg.snap->ps.clientNum && !cg.renderingThirdPerson) { if(cg_drawGun.integer == 2) @@ -999,22 +1000,87 @@ ByteToDir( es->eventParm, dir ); CG_MissileHitWall( es->weapon, es->clientNum, position, dir, IMPACTSOUND_DEFAULT ); } + */ + // if the client is us, unlagged is on server-side, and we've got it client-side + if ( es->clientNum == cg.predictedPlayerState.clientNum && + cgs.delagHitscan && (cg_delag.integer & 1 || cg_delag.integer & 16) ) { + // do nothing, because it was already predicted + //Com_Printf("Ignoring rail trail event\n"); + } + else { + // draw a rail trail, because it wasn't predicted + CG_RailTrail( ci, es->origin2, es->pos.trBase ); + + // if the end was on a nomark surface, don't make an explosion + if ( es->eventParm != 255 ) { + ByteToDir( es->eventParm, dir ); + CG_MissileHitWall( es->weapon, es->clientNum, position, dir, IMPACTSOUND_DEFAULT ); + } + //Com_Printf("Non-predicted rail trail\n"); + } + //unlagged - attack prediction #2 break; case EV_BULLET_HIT_WALL: DEBUGNAME("EV_BULLET_HIT_WALL"); + //unlagged - attack prediction #2 + /* ByteToDir( es->eventParm, dir ); CG_Bullet( es->pos.trBase, es->otherEntityNum, dir, qfalse, ENTITYNUM_WORLD ); + */ + // if the client is us, unlagged is on server-side, and we've got it client-side + if ( es->clientNum == cg.predictedPlayerState.clientNum && + cgs.delagHitscan && (cg_delag.integer & 1 || cg_delag.integer & 2) ) { + // do nothing, because it was already predicted + //Com_Printf("Ignoring bullet event\n"); + } + else { + // do the bullet, because it wasn't predicted + ByteToDir( es->eventParm, dir ); + CG_Bullet( es->pos.trBase, es->otherEntityNum, dir, qfalse, ENTITYNUM_WORLD ); + //Com_Printf("Non-predicted bullet\n"); + } + //unlagged - attack prediction #2 break; case EV_BULLET_HIT_FLESH: DEBUGNAME("EV_BULLET_HIT_FLESH"); + //unlagged - attack prediction #2 + /* CG_Bullet( es->pos.trBase, es->otherEntityNum, dir, qtrue, es->eventParm ); + */ + // if the client is us, unlagged is on server-side, and we've got it client-side + if ( es->clientNum == cg.predictedPlayerState.clientNum && + cgs.delagHitscan && (cg_delag.integer & 1 || cg_delag.integer & 2) ) { + // do nothing, because it was already predicted + //Com_Printf("Ignoring bullet event\n"); + } + else { + // do the bullet, because it wasn't predicted + CG_Bullet( es->pos.trBase, es->otherEntityNum, dir, qtrue, es->eventParm ); + //Com_Printf("Non-predicted bullet\n"); + } + //unlagged - attack prediction #2 break; case EV_SHOTGUN: DEBUGNAME("EV_SHOTGUN"); + //unlagged - attack prediction #2 + /* CG_ShotgunFire( es ); + */ + // if the client is us, unlagged is on server-side, and we've got it client-side + if ( es->otherEntityNum == cg.predictedPlayerState.clientNum && + cgs.delagHitscan && (cg_delag.integer & 1 || cg_delag.integer & 4) ) { + // do nothing, because it was already predicted + //Com_Printf("Ignoring shotgun event\n"); + } + else { + // do the shotgun pattern, because it wasn't predicted + CG_ShotgunFire( es ); + //Com_Printf("Non-predicted shotgun pattern\n"); + } + //unlagged - attack prediction #2 break; case EV_GENERAL_SOUND: diff -Nur ioq3/code/cgame/cg_freeze.c qlone-mod/code/cgame/cg_freeze.c --- ioq3/code/cgame/cg_freeze.c 1970-01-01 01:00:00.000000000 +0100 +++ qlone-mod/code/cgame/cg_freeze.c 2020-04-16 17:06:41.000000000 +0200 @@ -0,0 +1,136 @@ +#include "cg_local.h" + +void CG_Drop_f( void ) { + char command[ 128 ]; + char message[ 128 ]; + gitem_t *item; + int j; + + if ( cg.snap->ps.persistant[ PERS_TEAM ] == TEAM_SPECTATOR ) { + return; + } + if ( cg.predictedPlayerState.stats[ STAT_HEALTH ] <= 0 ) { + CG_Printf( "You must be alive to use this command.\n" ); + return; + } + trap_Args( message, 128 ); + item = BG_FindItem( message ); + if ( !item ) { + CG_Printf( "unknown item: %s\n", message ); + return; + } + + if ( !cg_items[ item->giTag ].registered ) { + return; + } + + j = item->giTag; + switch ( item->giType ) { + case IT_WEAPON: + if ( cgs.dmflags & 256 ) { + return; + } + if ( !( cg.snap->ps.stats[ STAT_WEAPONS ] & ( 1 << j ) ) ) { + CG_Printf( "Out of item: %s\n", message ); + return; + } + if ( cg.snap->ps.weaponstate != WEAPON_READY ) { + return; + } + if ( j == cg.snap->ps.weapon ) { + return; + } + if ( j <= WP_MACHINEGUN || j == WP_GRAPPLING_HOOK ) { + CG_Printf( "Item is not dropable.\n" ); + return; + } + case IT_AMMO: + if ( cg.snap->ps.ammo[ j ] < 1 ) { + CG_Printf( "Out of item: %s\n", message ); + return; + } + break; + case IT_POWERUP: + if ( cg.snap->ps.powerups[ j ] <= cg.time ) { + CG_Printf( "Out of item: %s\n", message ); + return; + } + break; + case IT_HOLDABLE: + if ( j == HI_KAMIKAZE ) { + CG_Printf( "Item is not dropable.\n" ); + return; + } + if ( bg_itemlist[ cg.snap->ps.stats[ STAT_HOLDABLE_ITEM ] ].giTag != j ) { + CG_Printf( "Out of item: %s\n", message ); + return; + } + break; + default: + CG_Printf( "Item is not dropable.\n" ); + return; + } + + Com_sprintf( command, 128, "drop %s", message ); + trap_SendClientCommand( command ); +} + +void CG_BodyObituary( entityState_t *ent, char *targetName ) { + int target, attacker; + char *message; + char *message2; + const char *attackerInfo; + char attackerName[ 32 ]; + gender_t gender; + char *s; + + target = ent->otherEntityNum; + attacker = ent->otherEntityNum2; + + attackerInfo = CG_ConfigString( CS_PLAYERS + attacker ); + if ( !attackerInfo ) return; + Q_strncpyz( attackerName, Info_ValueForKey( attackerInfo, "n" ), sizeof ( attackerName ) - 2 ); + strcat( attackerName, S_COLOR_WHITE ); + + if ( rand() & 1 ) { + message = "thawed"; + message2 = "like a package of frozen peas"; + } else { + gender = cgs.clientinfo[ target ].gender; + message = "evicted"; + if ( gender == GENDER_FEMALE ) { + message2 = "from her igloo"; + } else if ( gender == GENDER_NEUTER ) { + message2 = "from its igloo"; + } else { + message2 = "from his igloo"; + } + } + + if ( attacker == cg.snap->ps.clientNum ) { + s = va( "You thawed %s", targetName ); + CG_CenterPrint( s, SCREEN_HEIGHT * 0.25, BIGCHAR_WIDTH ); + } + if ( target == cg.snap->ps.clientNum ) { + s = va( "%s unfroze you", attackerName ); + CG_CenterPrint( s, SCREEN_HEIGHT * 0.25, BIGCHAR_WIDTH ); + } + CG_Printf( "%s %s %s %s.\n", attackerName, message, targetName, message2 ); +} + +qboolean Q_Isfreeze( int clientNum ) { + if ( cg.snap->ps.stats[ STAT_CLIENTS_READY ] & ( 1 << clientNum ) ) { + return ( !( cg.warmup || cg.predictedPlayerState.pm_type == PM_INTERMISSION ) ); + } + return qfalse; +} + +void CG_AddGib( localEntity_t *le ) { + const qhandle_t hShader = cgs.media.freezeShader; + + if ( le->refEntity.customShader == hShader ) { + le->refEntity.customShader = 0; + trap_R_AddRefEntityToScene( &le->refEntity ); + le->refEntity.customShader = hShader; + } +} diff -Nur ioq3/code/cgame/cg_local.h qlone-mod/code/cgame/cg_local.h --- ioq3/code/cgame/cg_local.h 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/cgame/cg_local.h 2020-04-15 09:59:26.000000000 +0200 @@ -446,6 +446,10 @@ // occurs, and they will have visible effects for #define STEP_TIME or whatever msec after #define MAX_PREDICTED_EVENTS 16 + +//unlagged - optimized prediction +#define NUM_SAVED_STATES (CMD_BACKUP + 2) +//unlagged - optimized prediction typedef struct { int clientFrame; // incremented each frame @@ -639,6 +643,12 @@ char testModelName[MAX_QPATH]; qboolean testGun; + //unlagged - optimized prediction + int lastPredictedCommand; + int lastServerTime; + playerState_t savedPmoveStates[NUM_SAVED_STATES]; + int stateHead, stateTail; + //unlagged - optimized prediction } cg_t; @@ -1072,6 +1082,10 @@ // media cgMedia_t media; + //unlagged - client options + // this will be set to the server's g_delagHitscan + int delagHitscan; + //unlagged - client options } cgs_t; //============================================================================== @@ -1157,7 +1171,10 @@ extern vmCvar_t cg_noVoiceText; #endif extern vmCvar_t cg_scorePlum; -extern vmCvar_t cg_smoothClients; +//unlagged - smooth clients #2 +// this is done server-side now +//extern vmCvar_t cg_smoothClients; +//unlagged - smooth clients #2 extern vmCvar_t pmove_fixed; extern vmCvar_t pmove_msec; //extern vmCvar_t cg_pmove_fixed; @@ -1191,6 +1208,26 @@ extern vmCvar_t cg_obeliskRespawnDelay; #endif +//unlagged - client options +extern vmCvar_t cg_delag; +extern vmCvar_t cg_debugDelag; +extern vmCvar_t cg_drawBBox; +extern vmCvar_t cg_cmdTimeNudge; +extern vmCvar_t sv_fps; +extern vmCvar_t cg_projectileNudge; +extern vmCvar_t cg_optimizePrediction; +extern vmCvar_t cl_timeNudge; +extern vmCvar_t cg_latentSnaps; +extern vmCvar_t cg_latentCmds; +extern vmCvar_t cg_plOut; +//unlagged - client options + +//unlagged - cg_unlagged.c +void CG_PredictWeaponEffects( centity_t *cent ); +void CG_AddBoundingBox( centity_t *cent ); +qboolean CG_Cvar_ClampInt( const char *name, vmCvar_t *vmCvar, int min, int max ); +//unlagged - cg_unlagged.c + // // cg_main.c // @@ -1429,6 +1466,9 @@ // cg_snapshot.c // void CG_ProcessSnapshots( void ); +//unlagged - early transitioning +void CG_TransitionEntity( centity_t *cent ); +//unlagged - early transitioning // // cg_info.c diff -Nur ioq3/code/cgame/cg_main.c qlone-mod/code/cgame/cg_main.c --- ioq3/code/cgame/cg_main.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/cgame/cg_main.c 2020-04-15 09:57:31.000000000 +0200 @@ -164,7 +164,10 @@ #endif vmCvar_t cg_hudFiles; vmCvar_t cg_scorePlum; -vmCvar_t cg_smoothClients; +//unlagged - smooth clients #2 +// this is done server-side now +//vmCvar_t cg_smoothClients; +//unlagged - smooth clients #2 vmCvar_t pmove_fixed; //vmCvar_t cg_pmove_fixed; vmCvar_t pmove_msec; @@ -199,6 +202,19 @@ vmCvar_t cg_recordSPDemoName; vmCvar_t cg_obeliskRespawnDelay; #endif +//unlagged - client options +vmCvar_t cg_delag; +vmCvar_t cg_debugDelag; +vmCvar_t cg_drawBBox; +vmCvar_t cg_cmdTimeNudge; +vmCvar_t sv_fps; +vmCvar_t cg_projectileNudge; +vmCvar_t cg_optimizePrediction; +vmCvar_t cl_timeNudge; +vmCvar_t cg_latentSnaps; +vmCvar_t cg_latentCmds; +vmCvar_t cg_plOut; +//unlagged - client options typedef struct { vmCvar_t *vmCvar; @@ -306,7 +322,10 @@ { &cg_timescaleFadeSpeed, "cg_timescaleFadeSpeed", "0", 0}, { &cg_timescale, "timescale", "1", 0}, { &cg_scorePlum, "cg_scorePlums", "1", CVAR_USERINFO | CVAR_ARCHIVE}, - { &cg_smoothClients, "cg_smoothClients", "0", CVAR_USERINFO | CVAR_ARCHIVE}, + //unlagged - smooth clients #2 + // this is done server-side now + //{ &cg_smoothClients, "cg_smoothClients", "0", CVAR_USERINFO | CVAR_ARCHIVE}, + //unlagged - smooth clients #2 { &cg_cameraMode, "com_cameraMode", "0", CVAR_CHEAT}, { &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO}, @@ -320,6 +339,20 @@ { &cg_oldRail, "cg_oldRail", "1", CVAR_ARCHIVE}, { &cg_oldRocket, "cg_oldRocket", "1", CVAR_ARCHIVE}, { &cg_oldPlasma, "cg_oldPlasma", "1", CVAR_ARCHIVE}, + //unlagged - client options + { &cg_delag, "cg_delag", "1", CVAR_ARCHIVE | CVAR_USERINFO }, + { &cg_debugDelag, "cg_debugDelag", "0", CVAR_USERINFO | CVAR_CHEAT }, + { &cg_drawBBox, "cg_drawBBox", "0", CVAR_CHEAT }, + { &cg_cmdTimeNudge, "cg_cmdTimeNudge", "0", CVAR_ARCHIVE | CVAR_USERINFO }, + // this will be automagically copied from the server + { &sv_fps, "sv_fps", "20", 0 }, + { &cg_projectileNudge, "cg_projectileNudge", "0", CVAR_ARCHIVE }, + { &cg_optimizePrediction, "cg_optimizePrediction", "1", CVAR_ARCHIVE }, + { &cl_timeNudge, "cl_timeNudge", "0", CVAR_ARCHIVE }, + { &cg_latentSnaps, "cg_latentSnaps", "0", CVAR_USERINFO | CVAR_CHEAT }, + { &cg_latentCmds, "cg_latentCmds", "0", CVAR_USERINFO | CVAR_CHEAT }, + { &cg_plOut, "cg_plOut", "0", CVAR_USERINFO | CVAR_CHEAT }, + //unlagged - client options { &cg_trueLightning, "cg_trueLightning", "0.0", CVAR_ARCHIVE} // { &cg_pmove_fixed, "cg_pmove_fixed", "0", CVAR_USERINFO | CVAR_ARCHIVE } }; @@ -382,6 +415,33 @@ cvarTable_t *cv; for ( i = 0, cv = cvarTable ; i < cvarTableSize ; i++, cv++ ) { + //unlagged - client options + // clamp the value between 0 and 999 + // negative values would suck - people could conceivably shoot other + // players *long* after they had left the area, on purpose + if ( cv->vmCvar == &cg_cmdTimeNudge ) { + CG_Cvar_ClampInt( cv->cvarName, cv->vmCvar, 0, 999 ); + } + // cl_timenudge less than -50 or greater than 50 doesn't actually + // do anything more than -50 or 50 (actually the numbers are probably + // closer to -30 and 30, but 50 is nice and round-ish) + // might as well not feed the myth, eh? + else if ( cv->vmCvar == &cl_timeNudge ) { + CG_Cvar_ClampInt( cv->cvarName, cv->vmCvar, -50, 50 ); + } + // don't let this go too high - no point + else if ( cv->vmCvar == &cg_latentSnaps ) { + CG_Cvar_ClampInt( cv->cvarName, cv->vmCvar, 0, 10 ); + } + // don't let this get too large + else if ( cv->vmCvar == &cg_latentCmds ) { + CG_Cvar_ClampInt( cv->cvarName, cv->vmCvar, 0, MAX_LATENT_CMDS - 1 ); + } + // no more than 100% packet loss + else if ( cv->vmCvar == &cg_plOut ) { + CG_Cvar_ClampInt( cv->cvarName, cv->vmCvar, 0, 100 ); + } + //unlagged - client options trap_Cvar_Update( cv->vmCvar ); } diff -Nur ioq3/code/cgame/cg_players.c qlone-mod/code/cgame/cg_players.c --- ioq3/code/cgame/cg_players.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/cgame/cg_players.c 2020-04-14 15:50:03.000000000 +0200 @@ -2591,6 +2591,11 @@ // add powerups floating behind the player CG_PlayerPowerups( cent, &torso ); + + //unlagged - client options + // add the bounding box (if cg_drawBBox is 1) + CG_AddBoundingBox( cent ); + //unlagged - client options } diff -Nur ioq3/code/cgame/cg_predict.c qlone-mod/code/cgame/cg_predict.c --- ioq3/code/cgame/cg_predict.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/cgame/cg_predict.c 2020-04-14 15:05:17.000000000 +0200 @@ -381,7 +381,135 @@ } } +//unlagged - optimized prediction +#define ABS(x) ((x) < 0 ? (-(x)) : (x)) +static int IsUnacceptableError( playerState_t *ps, playerState_t *pps ) { + vec3_t delta; + int i; + + if ( pps->pm_type != ps->pm_type || + pps->pm_flags != ps->pm_flags || + pps->pm_time != ps->pm_time ) { + return 1; + } + + VectorSubtract( pps->origin, ps->origin, delta ); + if ( VectorLengthSquared( delta ) > 0.1f * 0.1f ) { + if ( cg_showmiss.integer ) { + CG_Printf("delta: %.2f ", VectorLength(delta) ); + } + return 2; + } + + VectorSubtract( pps->velocity, ps->velocity, delta ); + if ( VectorLengthSquared( delta ) > 0.1f * 0.1f ) { + if ( cg_showmiss.integer ) { + CG_Printf("delta: %.2f ", VectorLength(delta) ); + } + return 3; + } + + if ( pps->weaponTime != ps->weaponTime || + pps->gravity != ps->gravity || + pps->speed != ps->speed || + pps->delta_angles[0] != ps->delta_angles[0] || + pps->delta_angles[1] != ps->delta_angles[1] || + pps->delta_angles[2] != ps->delta_angles[2] || + pps->groundEntityNum != ps->groundEntityNum ) { + return 4; + } + + if ( pps->legsTimer != ps->legsTimer || + pps->legsAnim != ps->legsAnim || + pps->torsoTimer != ps->torsoTimer || + pps->torsoAnim != ps->torsoAnim || + pps->movementDir != ps->movementDir ) { + return 5; + } + + VectorSubtract( pps->grapplePoint, ps->grapplePoint, delta ); + if ( VectorLengthSquared( delta ) > 0.1f * 0.1f ) { + return 6; + } + + if ( pps->eFlags != ps->eFlags ) { + return 7; + } + + if ( pps->eventSequence != ps->eventSequence ) { + return 8; + } + + for ( i = 0; i < MAX_PS_EVENTS; i++ ) { + if ( pps->events[i] != ps->events[i] || + pps->eventParms[i] != ps->eventParms[i] ) { + return 9; + } + } + + if ( pps->externalEvent != ps->externalEvent || + pps->externalEventParm != ps->externalEventParm || + pps->externalEventTime != ps->externalEventTime ) { + return 10; + } + + if ( pps->clientNum != ps->clientNum || + pps->weapon != ps->weapon || + pps->weaponstate != ps->weaponstate ) { + return 11; + } + + if ( ABS(pps->viewangles[0] - ps->viewangles[0]) > 1.0f || + ABS(pps->viewangles[1] - ps->viewangles[1]) > 1.0f || + ABS(pps->viewangles[2] - ps->viewangles[2]) > 1.0f ) { + return 12; + } + + if ( pps->viewheight != ps->viewheight ) { + return 13; + } + + if ( pps->damageEvent != ps->damageEvent || + pps->damageYaw != ps->damageYaw || + pps->damagePitch != ps->damagePitch || + pps->damageCount != ps->damageCount ) { + return 14; + } + + for ( i = 0; i < MAX_STATS; i++ ) { + if ( pps->stats[i] != ps->stats[i] ) { + return 15; + } + } + + for ( i = 0; i < MAX_PERSISTANT; i++ ) { + if ( pps->persistant[i] != ps->persistant[i] ) { + return 16; + } + } + + for ( i = 0; i < MAX_POWERUPS; i++ ) { + if ( pps->powerups[i] != ps->powerups[i] ) { + return 17; + } + } + + for ( i = 0; i < MAX_WEAPONS; i++ ) { + if ( pps->ammo[i] != ps->ammo[i] ) { + return 18; + } + } + + if ( pps->generic1 != ps->generic1 || + pps->loopSound != ps->loopSound || + pps->jumppad_ent != ps->jumppad_ent ) { + return 19; + } + + return 0; +} +//unlagged - optimized prediction /* ================= @@ -415,6 +543,10 @@ qboolean moved; usercmd_t oldestCmd; usercmd_t latestCmd; + //unlagged - optimized prediction + int stateIndex, predictCmd; + int numPredicted = 0, numPlayedBack = 0; // debug code + //unlagged - optimized prediction cg.hyperspace = qfalse; // will be set if touching a trigger_teleport @@ -499,6 +631,93 @@ cg_pmove.pmove_fixed = pmove_fixed.integer;// | cg_pmove_fixed.integer; cg_pmove.pmove_msec = pmove_msec.integer; + //unlagged - optimized prediction + // Like the comments described above, a player's state is entirely + // re-predicted from the last valid snapshot every client frame, which + // can be really, really, really slow. Every old command has to be + // run again. For every client frame that is *not* directly after a + // snapshot, this is unnecessary, since we have no new information. + // For those, we'll play back the predictions from the last frame and + // predict only the newest commands. Essentially, we'll be doing + // an incremental predict instead of a full predict. + // + // If we have a new snapshot, we can compare its player state's command + // time to the command times in the queue to find a match. If we find + // a matching state, and the predicted version has not deviated, we can + // use the predicted state as a base - and also do an incremental predict. + // + // With this method, we get incremental predicts on every client frame + // except a frame following a new snapshot in which there was a prediction + // error. This yeilds anywhere from a 15% to 40% performance increase, + // depending on how much of a bottleneck the CPU is. + + // we check for cg_latentCmds because it'll mess up the optimization + // FIXME: make cg_latentCmds work with cg_optimizePrediction? + if ( cg_optimizePrediction.integer && !cg_latentCmds.integer ) { + if ( cg.nextFrameTeleport || cg.thisFrameTeleport ) { + // do a full predict + cg.lastPredictedCommand = 0; + cg.stateTail = cg.stateHead; + predictCmd = current - CMD_BACKUP + 1; + } + // cg.physicsTime is the current snapshot's serverTime + // if it's the same as the last one + else if ( cg.physicsTime == cg.lastServerTime ) { + // we have no new information, so do an incremental predict + predictCmd = cg.lastPredictedCommand + 1; + } + else { + // we have a new snapshot + + int i; + qboolean error = qtrue; + + // loop through the saved states queue + for ( i = cg.stateHead; i != cg.stateTail; i = (i + 1) % NUM_SAVED_STATES ) { + // if we find a predicted state whose commandTime matches the snapshot player state's commandTime + if ( cg.savedPmoveStates[i].commandTime == cg.predictedPlayerState.commandTime ) { + // make sure the state differences are acceptable + int errorcode = IsUnacceptableError( &cg.predictedPlayerState, &cg.savedPmoveStates[i] ); + + // too much change? + if ( errorcode ) { + if ( cg_showmiss.integer ) { + CG_Printf("errorcode %d at %d\n", errorcode, cg.time); + } + // yeah, so do a full predict + break; + } + + // this one is almost exact, so we'll copy it in as the starting point + *cg_pmove.ps = cg.savedPmoveStates[i]; + // advance the head + cg.stateHead = (i + 1) % NUM_SAVED_STATES; + + // set the next command to predict + predictCmd = cg.lastPredictedCommand + 1; + + // a saved state matched, so flag it + error = qfalse; + break; + } + } + + // if no saved states matched + if ( error ) { + // do a full predict + cg.lastPredictedCommand = 0; + cg.stateTail = cg.stateHead; + predictCmd = current - CMD_BACKUP + 1; + } + } + + // keep track of the server time of the last snapshot so we + // know when we're starting from a new one in future calls + cg.lastServerTime = cg.physicsTime; + stateIndex = cg.stateHead; + } + //unlagged - optimized prediction + // run cmds moved = qfalse; for ( cmdNum = current - CMD_BACKUP + 1 ; cmdNum <= current ; cmdNum++ ) { @@ -581,7 +800,52 @@ cg_pmove.cmd.serverTime = ((cg_pmove.cmd.serverTime + pmove_msec.integer-1) / pmove_msec.integer) * pmove_msec.integer; } - Pmove (&cg_pmove); + //unlagged - optimized prediction + //Pmove (&cg_pmove); + // we check for cg_latentCmds because it'll mess up the optimization + if ( cg_optimizePrediction.integer && !cg_latentCmds.integer ) { + // if we need to predict this command, or we've run out of space in the saved states queue + if ( cmdNum >= predictCmd || (stateIndex + 1) % NUM_SAVED_STATES == cg.stateHead ) { + // run the Pmove + Pmove (&cg_pmove); + + numPredicted++; // debug code + + // record the last predicted command + cg.lastPredictedCommand = cmdNum; + + // if we haven't run out of space in the saved states queue + if ( (stateIndex + 1) % NUM_SAVED_STATES != cg.stateHead ) { + // save the state for the false case (of cmdNum >= predictCmd) + // in later calls to this function + cg.savedPmoveStates[stateIndex] = *cg_pmove.ps; + stateIndex = (stateIndex + 1) % NUM_SAVED_STATES; + cg.stateTail = stateIndex; + } + } + else { + numPlayedBack++; // debug code + + if ( cg_showmiss.integer && + cg.savedPmoveStates[stateIndex].commandTime != cg_pmove.cmd.serverTime) { + // this should ONLY happen just after changing the value of pmove_fixed + CG_Printf( "saved state miss\n" ); + } + + // play back the command from the saved states + *cg_pmove.ps = cg.savedPmoveStates[stateIndex]; + + // go to the next element in the saved states array + stateIndex = (stateIndex + 1) % NUM_SAVED_STATES; + } + } + else { + // run the Pmove + Pmove (&cg_pmove); + + numPredicted++; // debug code + } + //unlagged - optimized prediction moved = qtrue; @@ -592,6 +856,15 @@ //CG_CheckChangedPredictableEvents(&cg.predictedPlayerState); } + //unlagged - optimized prediction + // do a /condump after a few seconds of this + //CG_Printf("cg.time: %d, numPredicted: %d, numPlayedBack: %d\n", cg.time, numPredicted, numPlayedBack); // debug code + // if everything is working right, numPredicted should be 1 more than 98% + // of the time, meaning only ONE predicted move was done in the frame + // you should see other values for numPredicted after IsUnacceptableError + // returns nonzero, and that's it + //unlagged - optimized prediction + if ( cg_showmiss.integer > 1 ) { CG_Printf( "[%i : %i] ", cg_pmove.cmd.serverTime, cg.time ); } diff -Nur ioq3/code/cgame/cg_servercmds.c qlone-mod/code/cgame/cg_servercmds.c --- ioq3/code/cgame/cg_servercmds.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/cgame/cg_servercmds.c 2020-04-14 15:33:07.000000000 +0200 @@ -171,6 +171,11 @@ trap_Cvar_Set("g_redTeam", cgs.redTeam); Q_strncpyz( cgs.blueTeam, Info_ValueForKey( info, "g_blueTeam" ), sizeof(cgs.blueTeam) ); trap_Cvar_Set("g_blueTeam", cgs.blueTeam); + //unlagged - server options + // we'll need this for deciding whether or not to predict weapon effects + cgs.delagHitscan = atoi( Info_ValueForKey( info, "g_delagHitscan" ) ); + trap_Cvar_Set("g_delagHitscan", va("%i", cgs.delagHitscan)); + //unlagged - server options } /* diff -Nur ioq3/code/cgame/cg_snapshot.c qlone-mod/code/cgame/cg_snapshot.c --- ioq3/code/cgame/cg_snapshot.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/cgame/cg_snapshot.c 2020-04-15 09:18:24.000000000 +0200 @@ -55,7 +55,11 @@ cent->nextState is moved to cent->currentState and events are fired =============== */ -static void CG_TransitionEntity( centity_t *cent ) { +//unlagged - early transitioning +// used to be static, now needed to transition entities from within cg_ents.c +//static void CG_TransitionEntity( centity_t *cent ) { +void CG_TransitionEntity( centity_t *cent ) { +//unlagged - early transitioning cent->currentState = cent->nextState; cent->currentValid = qtrue; @@ -274,6 +278,24 @@ cgs.processedSnapshotNum++; r = trap_GetSnapshot( cgs.processedSnapshotNum, dest ); + //unlagged - lag simulation #1 + // the client wants latent snaps and the just-read snapshot is valid + if ( cg_latentSnaps.integer && r ) { + int i = 0, time = dest->serverTime; + + // keep grabbing one snapshot earlier until we get to the right time + while ( dest->serverTime > time - cg_latentSnaps.integer * (1000 / sv_fps.integer) ) { + if ( !(r = trap_GetSnapshot( cgs.processedSnapshotNum - i, dest )) ) { + // the snapshot is not valid, so stop here + break; + } + + // go back one more + i++; + } + } + //unlagged - lag simulation #1 + // FIXME: why would trap_GetSnapshot return a snapshot with the same server time if ( cg.snap && r && dest->serverTime == cg.snap->serverTime ) { //continue; @@ -330,7 +352,16 @@ if ( n != cg.latestSnapshotNum ) { if ( n < cg.latestSnapshotNum ) { // this should never happen - CG_Error( "CG_ProcessSnapshots: n < cg.latestSnapshotNum" ); + //unlagged - lag simulation #1 + //CG_Error( "CG_ProcessSnapshots: n < cg.latestSnapshotNum" ); + // this may actually happen with lag simulation going on + if ( cg_latentSnaps.integer ) { + CG_Printf( "WARNING: CG_ProcessSnapshots: n < cg.latestSnapshotNum\n" ); + } + else { + CG_Error( "CG_ProcessSnapshots: n < cg.latestSnapshotNum" ); + } + //unlagged - lag simulation #1 } cg.latestSnapshotNum = n; } @@ -371,7 +402,16 @@ // if time went backwards, we have a level restart if ( cg.nextSnap->serverTime < cg.snap->serverTime ) { - CG_Error( "CG_ProcessSnapshots: Server time went backwards" ); + //unlagged - lag simulation #1 + //CG_Error( "CG_ProcessSnapshots: Server time went backwards" ); + // this may actually happen with lag simulation going on + if ( cg_latentSnaps.integer ) { + CG_Printf( "WARNING: CG_ProcessSnapshots: Server time went backwards\n" ); + } + else { + CG_Error( "CG_ProcessSnapshots: Server time went backwards" ); + } + //unlagged - lag simulation #1 } } diff -Nur ioq3/code/cgame/cg_unlagged.c qlone-mod/code/cgame/cg_unlagged.c --- ioq3/code/cgame/cg_unlagged.c 1970-01-01 01:00:00.000000000 +0100 +++ qlone-mod/code/cgame/cg_unlagged.c 2020-04-15 10:42:23.000000000 +0200 @@ -0,0 +1,399 @@ +#include "cg_local.h" + +// we'll need these prototypes +void CG_ShotgunPattern( vec3_t origin, vec3_t origin2, int seed, int otherEntNum ); +void CG_Bullet( vec3_t end, int sourceEntityNum, vec3_t normal, qboolean flesh, int fleshEntityNum ); + +// and this as well +#define MACHINEGUN_SPREAD 200 + +/* +======================= +CG_PredictWeaponEffects + +Draws predicted effects for the railgun, shotgun, and machinegun. The +lightning gun is done in CG_LightningBolt, since it was just a matter +of setting the right origin and angles. +======================= +*/ +void CG_PredictWeaponEffects( centity_t *cent ) { + vec3_t muzzlePoint, forward, right, up; + entityState_t *ent = ¢->currentState; + + // if the client isn't us, forget it + if ( cent->currentState.number != cg.predictedPlayerState.clientNum ) { + return; + } + + // if it's not switched on server-side, forget it + if ( !cgs.delagHitscan ) { + return; + } + + // get the muzzle point + VectorCopy( cg.predictedPlayerState.origin, muzzlePoint ); + muzzlePoint[2] += cg.predictedPlayerState.viewheight; + + // get forward, right, and up + AngleVectors( cg.predictedPlayerState.viewangles, forward, right, up ); + VectorMA( muzzlePoint, 14, forward, muzzlePoint ); + + // was it a rail attack? + if ( ent->weapon == WP_RAILGUN ) { + // do we have it on for the rail gun? + if ( cg_delag.integer & 1 || cg_delag.integer & 16 ) { + trace_t trace; + vec3_t endPoint; + + // trace forward + VectorMA( muzzlePoint, 8192, forward, endPoint ); + + // THIS IS FOR DEBUGGING! + // you definitely *will* want something like this to test the backward reconciliation + // to make sure it's working *exactly* right + if ( cg_debugDelag.integer ) { + // trace forward + CG_Trace( &trace, muzzlePoint, vec3_origin, vec3_origin, endPoint, cent->currentState.number, CONTENTS_BODY|CONTENTS_SOLID ); + + // did we hit another player? + if ( trace.fraction < 1.0f && (trace.contents & CONTENTS_BODY) ) { + // if we have two snapshots (we're interpolating) + if ( cg.nextSnap ) { + centity_t *c = &cg_entities[trace.entityNum]; + vec3_t origin1, origin2; + + // figure the two origins used for interpolation + BG_EvaluateTrajectory( &c->currentState.pos, cg.snap->serverTime, origin1 ); + BG_EvaluateTrajectory( &c->nextState.pos, cg.nextSnap->serverTime, origin2 ); + + // print some debugging stuff exactly like what the server does + + // it starts with "Int:" to let you know the target was interpolated + CG_Printf("^3Int: time: %d, j: %d, k: %d, origin: %0.2f %0.2f %0.2f\n", + cg.oldTime, cg.snap->serverTime, cg.nextSnap->serverTime, + c->lerpOrigin[0], c->lerpOrigin[1], c->lerpOrigin[2]); + CG_Printf("^5frac: %0.4f, origin1: %0.2f %0.2f %0.2f, origin2: %0.2f %0.2f %0.2f\n", + cg.frameInterpolation, origin1[0], origin1[1], origin1[2], origin2[0], origin2[1], origin2[2]); + } + else { + // we haven't got a next snapshot + // the client clock has either drifted ahead (seems to happen once per server frame + // when you play locally) or the client is using timenudge + // in any case, CG_CalcEntityLerpPositions extrapolated rather than interpolated + centity_t *c = &cg_entities[trace.entityNum]; + vec3_t origin1, origin2; + + c->currentState.pos.trTime = TR_LINEAR_STOP; + c->currentState.pos.trTime = cg.snap->serverTime; + c->currentState.pos.trDuration = 1000 / sv_fps.integer; + + BG_EvaluateTrajectory( &c->currentState.pos, cg.snap->serverTime, origin1 ); + BG_EvaluateTrajectory( &c->currentState.pos, cg.snap->serverTime + 1000 / sv_fps.integer, origin2 ); + + // print some debugging stuff exactly like what the server does + + // it starts with "Ext:" to let you know the target was extrapolated + CG_Printf("^3Ext: time: %d, j: %d, k: %d, origin: %0.2f %0.2f %0.2f\n", + cg.oldTime, cg.snap->serverTime, cg.snap->serverTime, + c->lerpOrigin[0], c->lerpOrigin[1], c->lerpOrigin[2]); + CG_Printf("^5frac: %0.4f, origin1: %0.2f %0.2f %0.2f, origin2: %0.2f %0.2f %0.2f\n", + cg.frameInterpolation, origin1[0], origin1[1], origin1[2], origin2[0], origin2[1], origin2[2]); + } + } + } + + // find the rail's end point + CG_Trace( &trace, muzzlePoint, vec3_origin, vec3_origin, endPoint, cg.predictedPlayerState.clientNum, CONTENTS_SOLID ); + + // do the magic-number adjustment + VectorMA( muzzlePoint, 4, right, muzzlePoint ); + VectorMA( muzzlePoint, -1, up, muzzlePoint ); + + // draw a rail trail + CG_RailTrail( &cgs.clientinfo[cent->currentState.number], muzzlePoint, trace.endpos ); + //Com_Printf( "Predicted rail trail\n" ); + + // explosion at end if not SURF_NOIMPACT + if ( !(trace.surfaceFlags & SURF_NOIMPACT) ) { + // predict an explosion + CG_MissileHitWall( ent->weapon, cg.predictedPlayerState.clientNum, trace.endpos, trace.plane.normal, IMPACTSOUND_DEFAULT ); + } + } + } + // was it a shotgun attack? + else if ( ent->weapon == WP_SHOTGUN ) { + // do we have it on for the shotgun? + if ( cg_delag.integer & 1 || cg_delag.integer & 4 ) { + int contents; + vec3_t endPoint, v; + + // do everything like the server does + + SnapVector( muzzlePoint ); + + VectorScale( forward, 4096, endPoint ); + SnapVector( endPoint ); + + VectorSubtract( endPoint, muzzlePoint, v ); + VectorNormalize( v ); + VectorScale( v, 32, v ); + VectorAdd( muzzlePoint, v, v ); + + if ( cgs.glconfig.hardwareType != GLHW_RAGEPRO ) { + // ragepro can't alpha fade, so don't even bother with smoke + vec3_t up; + + contents = trap_CM_PointContents( muzzlePoint, 0 ); + if ( !( contents & CONTENTS_WATER ) ) { + VectorSet( up, 0, 0, 8 ); + CG_SmokePuff( v, up, 32, 1, 1, 1, 0.33f, 900, cg.time, 0, LEF_PUFF_DONT_SCALE, cgs.media.shotgunSmokePuffShader ); + } + } + + // do the shotgun pellets + CG_ShotgunPattern( muzzlePoint, endPoint, cg.oldTime % 256, cg.predictedPlayerState.clientNum ); + //Com_Printf( "Predicted shotgun pattern\n" ); + } + } + // was it a machinegun attack? + else if ( ent->weapon == WP_MACHINEGUN ) { + // do we have it on for the machinegun? + if ( cg_delag.integer & 1 || cg_delag.integer & 2 ) { + // the server will use this exact time (it'll be serverTime on that end) + int seed = cg.oldTime % 256; + float r, u; + trace_t tr; + qboolean flesh; + int fleshEntityNum; + vec3_t endPoint; + + // do everything exactly like the server does + + r = Q_random(&seed) * M_PI * 2.0f; + u = sin(r) * Q_crandom(&seed) * MACHINEGUN_SPREAD * 16; + r = cos(r) * Q_crandom(&seed) * MACHINEGUN_SPREAD * 16; + + VectorMA( muzzlePoint, 8192*16, forward, endPoint ); + VectorMA( endPoint, r, right, endPoint ); + VectorMA( endPoint, u, up, endPoint ); + + CG_Trace(&tr, muzzlePoint, NULL, NULL, endPoint, cg.predictedPlayerState.clientNum, MASK_SHOT ); + + if ( tr.surfaceFlags & SURF_NOIMPACT ) { + return; + } + + // snap the endpos to integers, but nudged towards the line + SnapVectorTowards( tr.endpos, muzzlePoint ); + + // do bullet impact + if ( tr.entityNum < MAX_CLIENTS ) { + flesh = qtrue; + fleshEntityNum = tr.entityNum; + } else { + flesh = qfalse; + } + + // do the bullet impact + CG_Bullet( tr.endpos, cg.predictedPlayerState.clientNum, tr.plane.normal, flesh, fleshEntityNum ); + //Com_Printf( "Predicted bullet\n" ); + } + } +} + +/* +================= +CG_AddBoundingBox + +Draws a bounding box around a player. Called from CG_Player. +================= +*/ +void CG_AddBoundingBox( centity_t *cent ) { + polyVert_t verts[4]; + clientInfo_t *ci; + int i; + vec3_t mins = {-15, -15, -24}; + vec3_t maxs = {15, 15, 32}; + float extx, exty, extz; + vec3_t corners[8]; + qhandle_t bboxShader, bboxShader_nocull; + + if ( !cg_drawBBox.integer ) { + return; + } + + // don't draw it if it's us in first-person + if ( cent->currentState.number == cg.predictedPlayerState.clientNum && + !cg.renderingThirdPerson ) { + return; + } + + // don't draw it for dead players + if ( cent->currentState.eFlags & EF_DEAD ) { + return; + } + + // get the shader handles + bboxShader = trap_R_RegisterShader( "bbox" ); + bboxShader_nocull = trap_R_RegisterShader( "bbox_nocull" ); + + // if they don't exist, forget it + if ( !bboxShader || !bboxShader_nocull ) { + return; + } + + // get the player's client info + ci = &cgs.clientinfo[cent->currentState.clientNum]; + + // if it's us + if ( cent->currentState.number == cg.predictedPlayerState.clientNum ) { + // use the view height + maxs[2] = cg.predictedPlayerState.viewheight + 6; + } + else { + int x, zd, zu; + + // otherwise grab the encoded bounding box + x = (cent->currentState.solid & 255); + zd = ((cent->currentState.solid>>8) & 255); + zu = ((cent->currentState.solid>>16) & 255) - 32; + + mins[0] = mins[1] = -x; + maxs[0] = maxs[1] = x; + mins[2] = -zd; + maxs[2] = zu; + } + + // get the extents (size) + extx = maxs[0] - mins[0]; + exty = maxs[1] - mins[1]; + extz = maxs[2] - mins[2]; + + + // set the polygon's texture coordinates + verts[0].st[0] = 0; + verts[0].st[1] = 0; + verts[1].st[0] = 0; + verts[1].st[1] = 1; + verts[2].st[0] = 1; + verts[2].st[1] = 1; + verts[3].st[0] = 1; + verts[3].st[1] = 0; + + // set the polygon's vertex colors + if ( ci->team == TEAM_RED ) { + for ( i = 0; i < 4; i++ ) { + verts[i].modulate[0] = 160; + verts[i].modulate[1] = 0; + verts[i].modulate[2] = 0; + verts[i].modulate[3] = 255; + } + } + else if ( ci->team == TEAM_BLUE ) { + for ( i = 0; i < 4; i++ ) { + verts[i].modulate[0] = 0; + verts[i].modulate[1] = 0; + verts[i].modulate[2] = 192; + verts[i].modulate[3] = 255; + } + } + else { + for ( i = 0; i < 4; i++ ) { + verts[i].modulate[0] = 0; + verts[i].modulate[1] = 128; + verts[i].modulate[2] = 0; + verts[i].modulate[3] = 255; + } + } + + VectorAdd( cent->lerpOrigin, maxs, corners[3] ); + + VectorCopy( corners[3], corners[2] ); + corners[2][0] -= extx; + + VectorCopy( corners[2], corners[1] ); + corners[1][1] -= exty; + + VectorCopy( corners[1], corners[0] ); + corners[0][0] += extx; + + for ( i = 0; i < 4; i++ ) { + VectorCopy( corners[i], corners[i + 4] ); + corners[i + 4][2] -= extz; + } + + // top + VectorCopy( corners[0], verts[0].xyz ); + VectorCopy( corners[1], verts[1].xyz ); + VectorCopy( corners[2], verts[2].xyz ); + VectorCopy( corners[3], verts[3].xyz ); + trap_R_AddPolyToScene( bboxShader, 4, verts ); + + // bottom + VectorCopy( corners[7], verts[0].xyz ); + VectorCopy( corners[6], verts[1].xyz ); + VectorCopy( corners[5], verts[2].xyz ); + VectorCopy( corners[4], verts[3].xyz ); + trap_R_AddPolyToScene( bboxShader, 4, verts ); + + // top side + VectorCopy( corners[3], verts[0].xyz ); + VectorCopy( corners[2], verts[1].xyz ); + VectorCopy( corners[6], verts[2].xyz ); + VectorCopy( corners[7], verts[3].xyz ); + trap_R_AddPolyToScene( bboxShader_nocull, 4, verts ); + + // left side + VectorCopy( corners[2], verts[0].xyz ); + VectorCopy( corners[1], verts[1].xyz ); + VectorCopy( corners[5], verts[2].xyz ); + VectorCopy( corners[6], verts[3].xyz ); + trap_R_AddPolyToScene( bboxShader_nocull, 4, verts ); + + // right side + VectorCopy( corners[0], verts[0].xyz ); + VectorCopy( corners[3], verts[1].xyz ); + VectorCopy( corners[7], verts[2].xyz ); + VectorCopy( corners[4], verts[3].xyz ); + trap_R_AddPolyToScene( bboxShader_nocull, 4, verts ); + + // bottom side + VectorCopy( corners[1], verts[0].xyz ); + VectorCopy( corners[0], verts[1].xyz ); + VectorCopy( corners[4], verts[2].xyz ); + VectorCopy( corners[5], verts[3].xyz ); + trap_R_AddPolyToScene( bboxShader_nocull, 4, verts ); +} + +/* +================ +CG_Cvar_ClampInt + +Clamps a cvar between two integer values, returns qtrue if it had to. +================ +*/ +qboolean CG_Cvar_ClampInt( const char *name, vmCvar_t *vmCvar, int min, int max ) { + if ( vmCvar->integer > max ) { + CG_Printf( "Allowed values are %d to %d.\n", min, max ); + + Com_sprintf( vmCvar->string, MAX_CVAR_VALUE_STRING, "%d", max ); + vmCvar->value = max; + vmCvar->integer = max; + + trap_Cvar_Set( name, vmCvar->string ); + return qtrue; + } + + if ( vmCvar->integer < min ) { + CG_Printf( "Allowed values are %d to %d.\n", min, max ); + + Com_sprintf( vmCvar->string, MAX_CVAR_VALUE_STRING, "%d", min ); + vmCvar->value = min; + vmCvar->integer = min; + + trap_Cvar_Set( name, vmCvar->string ); + return qtrue; + } + + return qfalse; +} diff -Nur ioq3/code/cgame/cg_view.c qlone-mod/code/cgame/cg_view.c --- ioq3/code/cgame/cg_view.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/cgame/cg_view.c 2020-04-14 14:48:58.000000000 +0200 @@ -762,6 +762,10 @@ int inwater; cg.time = serverTime; + //unlagged - lag simulation #1 + // adjust the clock to reflect latent snaps + cg.time -= cg_latentSnaps.integer * (1000 / sv_fps.integer); + //unlagged - lag simulation #1 cg.demoPlayback = demoPlayback; // update cvars diff -Nur ioq3/code/cgame/cg_weapons.c qlone-mod/code/cgame/cg_weapons.c --- ioq3/code/cgame/cg_weapons.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/cgame/cg_weapons.c 2020-04-15 10:17:48.000000000 +0200 @@ -977,13 +977,32 @@ memset( &beam, 0, sizeof( beam ) ); + //unlagged - attack prediction #1 + // if the entity is us, unlagged is on server-side, and we've got it on for the lightning gun + if ( (cent->currentState.number == cg.predictedPlayerState.clientNum) && cgs.delagHitscan && + ( cg_delag.integer & 1 || cg_delag.integer & 8 ) ) { + // always shoot straight forward from our current position + AngleVectors( cg.predictedPlayerState.viewangles, forward, NULL, NULL ); + VectorCopy( cg.predictedPlayerState.origin, muzzlePoint ); + } + else + //unlagged - attack prediction #1 // CPMA "true" lightning if ((cent->currentState.number == cg.predictedPlayerState.clientNum) && (cg_trueLightning.value != 0)) { vec3_t angle; int i; + //unlagged - true lightning + // might as well fix up true lightning while we're at it + vec3_t viewangles; + VectorCopy( cg.predictedPlayerState.viewangles, viewangles ); + //unlagged - true lightning + for (i = 0; i < 3; i++) { - float a = cent->lerpAngles[i] - cg.refdefViewAngles[i]; + //unlagged + //float a = cent->lerpAngles[i] - cg.refdefViewAngles[i]; + float a = cent->lerpAngles[i] - viewangles[i]; + //unlagged if (a > 180) { a -= 360; } @@ -991,7 +1010,10 @@ a += 360; } - angle[i] = cg.refdefViewAngles[i] + a * (1.0 - cg_trueLightning.value); + //unlagged + //angle[i] = cg.refdefViewAngles[i] + a * (1.0 - cg_trueLightning.value); + angle[i] = viewangles[i] + a * (1.0 - cg_trueLightning.value); + //unlagged if (angle[i] < 0) { angle[i] += 360; } @@ -1001,8 +1023,12 @@ } AngleVectors(angle, forward, NULL, NULL ); - VectorCopy(cent->lerpOrigin, muzzlePoint ); + //unlagged - true lightning + //VectorCopy(cent->lerpOrigin, muzzlePoint ); // VectorCopy(cg.refdef.vieworg, muzzlePoint ); + // *this* is the correct origin for true lightning + VectorCopy(cg.predictedPlayerState.origin, muzzlePoint ); + //unlagged - true lightning } else { // !CPMA AngleVectors( cent->lerpAngles, forward, NULL, NULL ); @@ -1728,6 +1754,10 @@ if ( weap->ejectBrassFunc && cg_brassTime.integer > 0 ) { weap->ejectBrassFunc( cent ); } + + //unlagged - attack prediction #1 + CG_PredictWeaponEffects( cent ); + //unlagged - attack prediction #1 } @@ -1979,7 +2009,11 @@ CG_ShotgunPellet ================ */ -static void CG_ShotgunPellet( vec3_t start, vec3_t end, int skipNum ) { +//unlagged - attack prediction +// made this non-static for access from cg_unlagged.c +//static void CG_ShotgunPellet( vec3_t start, vec3_t end, int skipNum ) { +void CG_ShotgunPellet( vec3_t start, vec3_t end, int skipNum ) { +//unlagged - attack prediction trace_t tr; int sourceContentType, destContentType; diff -Nur ioq3/code/game/ai_dmnet.c qlone-mod/code/game/ai_dmnet.c --- ioq3/code/game/ai_dmnet.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/game/ai_dmnet.c 2020-05-06 12:02:51.000000000 +0200 @@ -1292,6 +1292,10 @@ ================== */ int BotSelectActivateWeapon(bot_state_t *bs) { + //qlone - instaGib + if (g_instaGib.integer) + return WEAPONINDEX_RAILGUN; + //qlone - instaGib // if (bs->inventory[INVENTORY_MACHINEGUN] > 0 && bs->inventory[INVENTORY_BULLETS] > 0) return WEAPONINDEX_MACHINEGUN; diff -Nur ioq3/code/game/ai_dmq3.c qlone-mod/code/game/ai_dmq3.c --- ioq3/code/game/ai_dmq3.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/game/ai_dmq3.c 2020-05-06 12:05:39.000000000 +0200 @@ -377,7 +377,14 @@ teamtask = TEAMTASK_ESCORT; } else { - teamtask = TEAMTASK_FOLLOW; + //freeze + //qlone - optional FreezeTag + if ( g_freezeTag.integer && bs->formation_dist == 70 ) + //qlone - optional FreezeTag + teamtask = TEAMTASK_ESCORT; + else + //freeze + teamtask = TEAMTASK_FOLLOW; } break; case LTG_DEFENDKEYAREA: @@ -488,6 +495,10 @@ } } +//freeze +void BotTeamSeekGoals( bot_state_t *bs ); +//freeze + /* ================== BotCTFSeekGoals @@ -536,15 +547,28 @@ } return; } + // if the bot decided to follow someone - if ( bs->ltgtype == LTG_TEAMACCOMPANY && !bs->ordered ) { - // if the team mate being accompanied no longer carries the flag - BotEntityInfo(bs->teammate, &entinfo); - if (!EntityCarriesFlag(&entinfo)) { - bs->ltgtype = 0; + //qlone - optional FreezeTag + if (g_freezeTag.integer) { + //freeze + BotTeamSeekGoals( bs ); + if ( bs->ltgtype == LTG_TEAMACCOMPANY ) { + return; } + //freeze } - // + else { + //qlone - optional FreezeTag + if ( bs->ltgtype == LTG_TEAMACCOMPANY && !bs->ordered ) { + // if the team mate being accompanied no longer carries the flag + BotEntityInfo(bs->teammate, &entinfo); + if (!EntityCarriesFlag(&entinfo)) { + bs->ltgtype = 0; + } + } + } + if (BotTeam(bs) == TEAM_RED) flagstatus = bs->redflagstatus * 2 + bs->blueflagstatus; else flagstatus = bs->blueflagstatus * 2 + bs->redflagstatus; //if our team has the enemy flag and our flag is at the base @@ -1360,6 +1384,13 @@ BotHarvesterSeekGoals(bs); } #endif + //freeze + //qlone - optional FreezeTag + else if ( g_freezeTag.integer && gametype == GT_TEAM ) { + //qlone - optional FreezeTag + BotTeamSeekGoals( bs ); + } + //freeze } // reset the order time which is used to see if // we decided to refuse an order @@ -1393,7 +1424,13 @@ char buf[MAX_INFO_STRING]; if (client < 0 || client >= MAX_CLIENTS) { - BotAI_Print(PRT_ERROR, "ClientName: client out of range\n"); + //freeze + //qlone - optional FreezeTag + if (!g_freezeTag.integer) { + //qlone - optional FreezeTag + BotAI_Print(PRT_ERROR, "ClientName: client out of range\n"); + } + //freeze return "[client out of range]"; } trap_GetConfigstring(CS_PLAYERS+client, buf, sizeof(buf)); @@ -1569,7 +1606,13 @@ trap_EA_SelectWeapon(bs->client, bs->weaponnum); } else { - newweaponnum = trap_BotChooseBestFightWeapon(bs->ws, bs->inventory); + //qlone - instaGib + if (g_instaGib.integer) { + newweaponnum = WP_RAILGUN; // we only have the RG to choose + } else { + newweaponnum = trap_BotChooseBestFightWeapon(bs->ws, bs->inventory); + } + //qlone - instaGib if (bs->weaponnum != newweaponnum) bs->weaponchange_time = FloatTime(); bs->weaponnum = newweaponnum; //BotAI_Print(PRT_MESSAGE, "bs->weaponnum = %d\n", bs->weaponnum); @@ -2195,6 +2238,14 @@ ================== */ float BotAggression(bot_state_t *bs) { + //qlone - instaGib + if (g_instaGib.integer) { + //if the enemy is located way higher than the bot + if (bs->inventory[ENEMY_HEIGHT] > 200) return 0; + return 100; + } + //qlone - instaGib + //if the bot has quad if (bs->inventory[INVENTORY_QUAD]) { //if the bot is not holding the gauntlet or the enemy is really nearby @@ -2411,6 +2462,9 @@ ================== */ int BotHasPersistantPowerupAndWeapon(bot_state_t *bs) { + //qlone - instaGib + if (g_instaGib.integer) return qtrue; + //qlone - instaGib #ifdef MISSIONPACK // if the bot does not have a persistant powerup if (!bs->inventory[INVENTORY_SCOUT] && @@ -3002,6 +3056,10 @@ if (EntityIsInvisible(&entinfo) && !EntityIsShooting(&entinfo)) { continue; } + //unlagged - misc + // this has nothing to do with lag compensation, but it's great for testing + if ( g_entities[i].flags & FL_NOTARGET ) continue; + //unlagged - misc //if not an easy fragger don't shoot at chatting players if (easyfragger < 0.5 && EntityIsChatting(&entinfo)) continue; // @@ -3066,6 +3124,14 @@ float vis; aas_entityinfo_t entinfo; + //freeze + //qlone - optional FreezeTag + if ( g_freezeTag.integer && gametype == GT_CTF ) { + //qlone - optional FreezeTag + return -1; + } + //freeze + for (i = 0; i < level.maxclients; i++) { if (i == bs->client) continue; diff -Nur ioq3/code/game/ai_freeze.c qlone-mod/code/game/ai_freeze.c --- ioq3/code/game/ai_freeze.c 1970-01-01 01:00:00.000000000 +0100 +++ qlone-mod/code/game/ai_freeze.c 2020-04-16 17:19:36.000000000 +0200 @@ -0,0 +1,123 @@ +#include "g_local.h" +#include "../botlib/botlib.h" +#include "../botlib/be_aas.h" +#include "../botlib/be_ai_goal.h" +#include "../botlib/be_ai_move.h" + +#include "ai_main.h" +#include "ai_dmq3.h" + +#include "chars.h" + +int BotTeamCarrierVisible( bot_state_t *bs ) { + int i; + float vis; + aas_entityinfo_t entinfo; + gentity_t *ent; + float f, alertness; + float squaredist; + vec3_t dir; + int areanum; + + alertness = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_ALERTNESS, 0, 1 ); + + //qlone + //for ( i = 0; i < maxclients && i < MAX_CLIENTS; i++ ) { + for ( i = 0; i < level.maxclients && i < MAX_CLIENTS; i++ ) { + //qlone + if ( i == bs->client ) continue; + ent = &g_entities[ i ]; + if ( !ent->inuse || !ent->target_ent ) continue; + BotEntityInfo( ent->target_ent->s.number, &entinfo ); + if ( !entinfo.valid || !( entinfo.powerups & ( 1 << PW_BATTLESUIT ) ) ) continue; + if ( !BotSameTeam( bs, i ) ) continue; + VectorSubtract( entinfo.origin, bs->origin, dir ); + squaredist = VectorLengthSquared( dir ); + if ( squaredist > Square( 900.0 + alertness * 4000.0 ) ) continue; + f = 90 + 90 - ( 90 - ( squaredist > Square( 810 ) ? Square( 810 ) : squaredist ) / ( 810 * 9 ) ); + vis = BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, f, entinfo.number ); + if ( vis <= 0 ) continue; + areanum = BotPointAreaNum( entinfo.origin ); + if ( trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, areanum, bs->tfl ) ) { + return entinfo.number; + } + } + return -1; +} + +void BotRefuseOrder( bot_state_t *bs ); + +void BotTeamSeekGoals( bot_state_t *bs ) { + aas_entityinfo_t entinfo; + int c; + int areanum; + + if ( bs->ltgtype == LTG_TEAMACCOMPANY && !bs->ordered ) { + BotEntityInfo( bs->teammate, &entinfo ); + if ( !entinfo.valid || !( entinfo.powerups & ( 1 << PW_BATTLESUIT ) ) ) { + bs->ltgtype = 0; + return; + } + areanum = BotPointAreaNum( entinfo.origin ); + if ( !trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, areanum, bs->tfl ) ) { + bs->ltgtype = 0; + } + return; + } + if ( bs->owndecision_time < FloatTime() ) { + c = BotTeamCarrierVisible( bs ); + if ( c >= 0 && ( bs->ltgtype != LTG_TEAMACCOMPANY || bs->teammate != c ) ) { + BotRefuseOrder( bs ); + bs->decisionmaker = bs->client; + bs->ordered = qfalse; + bs->teammate = c; + bs->teammatevisible_time = FloatTime(); + bs->teammessage_time = 0; + bs->arrive_time = 1; + + bs->teamgoal_time = FloatTime() + 120; + bs->ltgtype = LTG_TEAMACCOMPANY; + bs->formation_dist = 70; + BotSetTeamStatus( bs ); + bs->owndecision_time = FloatTime() + 5; + return; + } + } +#if 0 + if ( BotTeamLeader( bs ) ) { + return; + } + if ( bs->lastgoal_ltgtype ) { + bs->teamgoal_time += 60; + } + if ( !bs->ordered && bs->lastgoal_ltgtype ) { + bs->ltgtype = 0; + } + if ( bs->ltgtype == LTG_TEAMHELP || + bs->ltgtype == LTG_TEAMACCOMPANY || + bs->ltgtype == LTG_DEFENDKEYAREA || + bs->ltgtype == LTG_CAMPORDER || + bs->ltgtype == LTG_PATROL || + bs->ltgtype == LTG_GETITEM ) { + return; + } + if ( BotSetLastOrderedTask( bs ) ) { + return; + } + if ( bs->owndecision_time > FloatTime() ) { + return; + } + if ( bs->ctfroam_time > FloatTime() ) { + return; + } + if ( BotAggression( bs ) < 50 ) { + return; + } + bs->teammessage_time = FloatTime() + 2 * random(); + + bs->ltgtype = 0; + bs->ctfroam_time = FloatTime() + CTF_ROAM_TIME; + BotSetTeamStatus( bs ); + bs->owndecision_time = FloatTime() + 5; +#endif +} diff -Nur ioq3/code/game/ai_main.c qlone-mod/code/game/ai_main.c --- ioq3/code/game/ai_main.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/game/ai_main.c 2020-05-07 11:49:18.000000000 +0200 @@ -185,7 +185,11 @@ memset( state, 0, sizeof(entityState_t) ); if (!ent->inuse) return qfalse; if (!ent->r.linked) return qfalse; - if (ent->r.svFlags & SVF_NOCLIENT) return qfalse; + //qlone - instaGib or no items on map + if (!g_removeAllItems.integer && !g_instaGib.integer) { + if (ent->r.svFlags & SVF_NOCLIENT) return qfalse; + } + //qlone - instaGib or no items on map memcpy( state, &ent->s, sizeof(entityState_t) ); return qtrue; } @@ -1492,10 +1496,14 @@ trap_BotLibUpdateEntity(i, NULL); continue; } - if (ent->r.svFlags & SVF_NOCLIENT) { - trap_BotLibUpdateEntity(i, NULL); - continue; + //qlone - instaGib or no items on map + if (!g_removeAllItems.integer && !g_instaGib.integer) { + if (ent->r.svFlags & SVF_NOCLIENT) { + trap_BotLibUpdateEntity(i, NULL); + continue; + } } + //qlone - instaGib or no items on map // do not update missiles if (ent->s.eType == ET_MISSILE && ent->s.weapon != WP_GRAPPLING_HOOK) { trap_BotLibUpdateEntity(i, NULL); diff -Nur ioq3/code/game/bg_misc.c qlone-mod/code/game/bg_misc.c --- ioq3/code/game/bg_misc.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/game/bg_misc.c 2020-05-18 14:09:11.000000000 +0200 @@ -24,6 +24,9 @@ #include "../qcommon/q_shared.h" #include "bg_public.h" +//qlone +#include "bg_qlone.h" +//qlone /*QUAKED item_***** ( 0 0 0 ) (-16 -16 -16) (16 16 16) suspended DO NOT USE THIS CLASS, IT JUST HOLDS GENERAL INFORMATION. @@ -1027,7 +1030,6 @@ } - /* ================ BG_CanItemBeGrabbed @@ -1046,10 +1048,21 @@ Com_Error( ERR_DROP, "BG_CanItemBeGrabbed: index out of range" ); } + //freeze + if ( ent->modelindex2 == 1 && ent->otherEntityNum == ps->clientNum + 1 ) { + return qfalse; + } + //freeze + item = &bg_itemlist[ent->modelindex]; switch( item->giType ) { case IT_WEAPON: + //freeze + if ( ent->modelindex2 == 255 && ps->stats[ STAT_WEAPONS ] & ( 1 << item->giTag ) ) { + return qfalse; + } + //freeze return qtrue; // weapons are always picked up case IT_AMMO: diff -Nur ioq3/code/game/bg_pmove.c qlone-mod/code/game/bg_pmove.c --- ioq3/code/game/bg_pmove.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/game/bg_pmove.c 2020-05-15 16:50:03.000000000 +0200 @@ -27,6 +27,22 @@ #include "bg_public.h" #include "bg_local.h" +//qlone +#include "bg_qlone.h" +/* +// thanx to Mr Pants "Excessive" mod +int trap_Cvar_VariableIntegerValue(const char * var_name); +const char *CG_ConfigString(int index); +int getCvarInt(const char * name) { +#ifdef CGAME + return atoi(Info_ValueForKey(CG_ConfigString(CS_SERVERINFO), name)); +#else + return trap_Cvar_VariableIntegerValue(name); +#endif +} +*/ +//qlone + pmove_t *pm; pml_t pml; @@ -1480,7 +1496,10 @@ PM_AddEvent( EV_CHANGE_WEAPON ); pm->ps->weaponstate = WEAPON_DROPPING; - pm->ps->weaponTime += 200; + //qlone - fast weapon switching (a la CPM) + //pm->ps->weaponTime += 200; + pm->ps->weaponTime += getCvarInt("g_fastWeaponSwitch") > 1 ? 0 : 200; + //qlone - fast weapon switching (a la CPM) PM_StartTorsoAnim( TORSO_DROP ); } @@ -1504,7 +1523,10 @@ pm->ps->weapon = weapon; pm->ps->weaponstate = WEAPON_RAISING; - pm->ps->weaponTime += 250; + //qlone - fast weapon switching (a la CPM) + //pm->ps->weaponTime += 250; + pm->ps->weaponTime += getCvarInt("g_fastWeaponSwitch") > 1 ? 0 : 250; + //qlone - fast weapon switching (a la CPM) PM_StartTorsoAnim( TORSO_RAISE ); } @@ -1526,7 +1548,6 @@ } } - /* ============== PM_Weapon @@ -1630,7 +1651,10 @@ // check for out of ammo if ( ! pm->ps->ammo[ pm->ps->weapon ] ) { PM_AddEvent( EV_NOAMMO ); - pm->ps->weaponTime += 500; + //qlone - fast weapon switching (a la CPM) + //pm->ps->weaponTime += 500; + pm->ps->weaponTime += getCvarInt("g_fastWeaponSwitch") > 0 ? 100 : 500; + //qlone - fast weapon switching (a la CPM) return; } @@ -1667,6 +1691,10 @@ break; case WP_RAILGUN: addTime = 1500; + //qlone - fast RG reload (a la CPM) + if (getCvarInt("g_fastWeaponSwitch") || getCvarInt("g_fastRGReload")) + addTime = 1000; + //qlone - fast RG reload (a la CPM) break; case WP_BFG: addTime = 200; diff -Nur ioq3/code/game/bg_qlone.c qlone-mod/code/game/bg_qlone.c --- ioq3/code/game/bg_qlone.c 1970-01-01 01:00:00.000000000 +0100 +++ qlone-mod/code/game/bg_qlone.c 2020-05-15 16:59:55.000000000 +0200 @@ -0,0 +1,14 @@ +#include "g_local.h" +#include "bg_qlone.h" + +// Allows to access an integer cvar +// thanx to Mr Pants "Excessive" mod +//int trap_Cvar_VariableIntegerValue(const char * var_name); +//const char *CG_ConfigString(int index); +int getCvarInt(const char * name) { +#ifdef CGAME + return atoi(Info_ValueForKey(CG_ConfigString(CS_SERVERINFO), name)); +#else + return trap_Cvar_VariableIntegerValue(name); +#endif +} diff -Nur ioq3/code/game/bg_qlone.h qlone-mod/code/game/bg_qlone.h --- ioq3/code/game/bg_qlone.h 1970-01-01 01:00:00.000000000 +0100 +++ qlone-mod/code/game/bg_qlone.h 2020-05-15 16:54:05.000000000 +0200 @@ -0,0 +1,4 @@ +// The 2 following declarations are needeed for getCvarInt() +int trap_Cvar_VariableIntegerValue(const char * var_name); +const char *CG_ConfigString(int index); +int getCvarInt(const char * name); diff -Nur ioq3/code/game/g_active.c qlone-mod/code/game/g_active.c --- ioq3/code/game/g_active.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/game/g_active.c 2020-04-20 15:43:33.000000000 +0200 @@ -270,7 +270,10 @@ } // ignore most entities if a spectator - if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + //freeze + //if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + if ( is_spectator( ent->client ) ) { + //freeze if ( hit->s.eType != ET_TELEPORT_TRIGGER && // this is ugly but adding a new ET_? type will // most likely cause network incompatibilities @@ -338,6 +341,13 @@ pm.ps = &client->ps; pm.cmd = *ucmd; pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; // spectators can fly through bodies + //freeze + //qlone - optional FreezeTag + if ( g_freezeTag.integer && g_dmflags.integer & 512 ) { + //qlone - optional FreezeTag + pm.tracemask &= ~CONTENTS_PLAYERCLIP; + } + //freeze pm.trace = trap_Trace; pm.pointcontents = trap_PointContents; @@ -357,6 +367,13 @@ if ( ( client->buttons & BUTTON_ATTACK ) && ! ( client->oldbuttons & BUTTON_ATTACK ) ) { Cmd_FollowCycle_f( ent, 1 ); } + //freeze + //qlone - optional FreezeTag + else if (g_freezeTag.integer) { + respawnSpectator( ent ); + } + //qlone - optional FreezeTag + //freeze } @@ -381,6 +398,13 @@ client->inactivityTime = level.time + g_inactivity.integer * 1000; client->inactivityWarning = qfalse; } else if ( !client->pers.localClient ) { + //freeze + //qlone - optional FreezeTag + if ( g_freezeTag.integer && g_entities[ client->ps.clientNum ].freezeState ) { + return qtrue; + } + //qlone - optional FreezeTag + //freeze if ( level.time > client->inactivityTime ) { trap_DropClient( client - level.clients, "Dropped due to inactivity" ); return qfalse; @@ -777,6 +801,110 @@ // G_Printf("serverTime >>>>>\n" ); } + //unlagged - backward reconciliation #4 + // frameOffset should be about the number of milliseconds into a frame + // this command packet was received, depending on how fast the server + // does a G_RunFrame() + client->frameOffset = trap_Milliseconds() - level.frameStartTime; + //unlagged - backward reconciliation #4 + + //unlagged - lag simulation #3 + // if the client wants to simulate outgoing packet loss + if ( client->pers.plOut ) { + // see if a random value is below the threshhold + float thresh = (float)client->pers.plOut / 100.0f; + if ( random() < thresh ) { + // do nothing at all if it is - this is a lost command + return; + } + } + //unlagged - lag simulation #3 + + //unlagged - true ping + // save the estimated ping in a queue for averaging later + + // we use level.previousTime to account for 50ms lag correction + // besides, this will turn out numbers more like what players are used to + client->pers.pingsamples[client->pers.samplehead] = level.previousTime + client->frameOffset - ucmd->serverTime; + client->pers.samplehead++; + if ( client->pers.samplehead >= NUM_PING_SAMPLES ) { + client->pers.samplehead -= NUM_PING_SAMPLES; + } + + // initialize the real ping + if ( g_truePing.integer ) { + int i, sum = 0; + + // get an average of the samples we saved up + for ( i = 0; i < NUM_PING_SAMPLES; i++ ) { + sum += client->pers.pingsamples[i]; + } + + client->pers.realPing = sum / NUM_PING_SAMPLES; + } + else { + // if g_truePing is off, use the normal ping + client->pers.realPing = client->ps.ping; + } + //unlagged - true ping + + //unlagged - lag simulation #2 + // keep a queue of past commands + client->pers.cmdqueue[client->pers.cmdhead] = client->pers.cmd; + client->pers.cmdhead++; + if ( client->pers.cmdhead >= MAX_LATENT_CMDS ) { + client->pers.cmdhead -= MAX_LATENT_CMDS; + } + + // if the client wants latency in commands (client-to-server latency) + if ( client->pers.latentCmds ) { + // save the actual command time + int time = ucmd->serverTime; + + // find out which index in the queue we want + int cmdindex = client->pers.cmdhead - client->pers.latentCmds - 1; + while ( cmdindex < 0 ) { + cmdindex += MAX_LATENT_CMDS; + } + + // read in the old command + client->pers.cmd = client->pers.cmdqueue[cmdindex]; + + // adjust the real ping to reflect the new latency + client->pers.realPing += time - ucmd->serverTime; + } + //unlagged - lag simulation #2 + + //unlagged - backward reconciliation #4 + // save the command time *before* pmove_fixed messes with the serverTime, + // and *after* lag simulation messes with it :) + // attackTime will be used for backward reconciliation later (time shift) + client->attackTime = ucmd->serverTime; + //unlagged - backward reconciliation #4 + + //unlagged - smooth clients #1 + // keep track of this for later - we'll use this to decide whether or not + // to send extrapolated positions for this client + client->lastUpdateFrame = level.framenum; + //unlagged - smooth clients #1 + + //unlagged - lag simulation #1 + // if the client is adding latency to received snapshots (server-to-client latency) + if ( client->pers.latentSnaps ) { + // adjust the real ping + client->pers.realPing += client->pers.latentSnaps * (1000 / sv_fps.integer); + // adjust the attack time so backward reconciliation will work + client->attackTime -= client->pers.latentSnaps * (1000 / sv_fps.integer); + } + //unlagged - lag simulation #1 + + //unlagged - true ping + // make sure the true ping is over 0 - with cl_timenudge it can be less + if ( client->pers.realPing < 0 ) { + client->pers.realPing = 0; + } + //unlagged - true ping + msec = ucmd->serverTime - client->ps.commandTime; // following others may result in bad times, but we still want // to check for follow toggles @@ -811,7 +939,10 @@ } // spectators don't do much - if ( client->sess.sessionTeam == TEAM_SPECTATOR ) { + //freeze + //if ( client->sess.sessionTeam == TEAM_SPECTATOR ) { + if ( is_spectator( client ) ) { + //freeze if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) { return; } @@ -857,6 +988,13 @@ client->hook && !( ucmd->buttons & BUTTON_ATTACK ) ) { Weapon_HookFree(client->hook); } + //freeze + //qlone - optional FreezeTag + if (g_freezeTag.integer) { + Hook_Fire( ent ); + } + //qlone - optional FreezeTag + //freeze // set up for pmove oldEventSequence = client->ps.eventSequence; @@ -912,6 +1050,13 @@ } else { pm.tracemask = MASK_PLAYERSOLID; + //freeze + //qlone - optional FreezeTag + if ( g_freezeTag.integer && g_dmflags.integer & 512 ) { + //qlone - optional FreezeTag + pm.tracemask &= ~CONTENTS_PLAYERCLIP; + } + //freeze } pm.trace = trap_Trace; pm.pointcontents = trap_PointContents; @@ -945,12 +1090,20 @@ if ( ent->client->ps.eventSequence != oldEventSequence ) { ent->eventTime = level.time; } + //unlagged - smooth clients #2 + // clients no longer do extrapolation if cg_smoothClients is 1, because + // skip correction is all handled server-side now + // since that's the case, it makes no sense to store the extra info + // in the client's snapshot entity, so let's save a little bandwidth + /* if (g_smoothClients.integer) { BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); } else { + */ BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); - } + /*}*/ + //unlagged - smooth clients #2 SendPendingPredictableEvents( &ent->client->ps ); if ( !( ent->client->ps.eFlags & EF_FIRING ) ) { @@ -1030,9 +1183,14 @@ ent = g_entities + clientNum; trap_GetUsercmd( clientNum, &ent->client->pers.cmd ); + //unlagged - smooth clients #1 + /* + // this is handled differently now // mark the time we got info, so we can display the // phone jack if they don't get any for a while ent->client->lastCmdTime = level.time; + */ + //unlagged - smooth clients #1 if ( !(ent->r.svFlags & SVF_BOT) && !g_synchronousClients.integer ) { ClientThink_real( ent ); @@ -1072,9 +1230,21 @@ } if ( clientNum >= 0 ) { cl = &level.clients[ clientNum ]; - if ( cl->pers.connected == CON_CONNECTED && cl->sess.sessionTeam != TEAM_SPECTATOR ) { + //freeze + //if ( cl->pers.connected == CON_CONNECTED && cl->sess.sessionTeam != TEAM_SPECTATOR ) { + if ( cl->pers.connected == CON_CONNECTED && !is_spectator( cl ) ) { + //freeze flags = (cl->ps.eFlags & ~(EF_VOTED | EF_TEAMVOTED)) | (ent->client->ps.eFlags & (EF_VOTED | EF_TEAMVOTED)); - ent->client->ps = cl->ps; + //qlone - optional FreezeTag + if (g_freezeTag.integer) { + //freeze + Persistant_spectator( ent, cl ); + //freeze + } + else { + ent->client->ps = cl->ps; + } + //qlone - optional FreezeTag ent->client->ps.pm_flags |= PMF_FOLLOW; ent->client->ps.eFlags = flags; return; @@ -1082,12 +1252,22 @@ } if ( ent->client->ps.pm_flags & PMF_FOLLOW ) { - // drop them to free spectators unless they are dedicated camera followers - if ( ent->client->sess.spectatorClient >= 0 ) { - ent->client->sess.spectatorState = SPECTATOR_FREE; + //qlone - optional FreezeTag + if (!g_freezeTag.integer) { + // drop them to free spectators unless they are dedicated camera followers + if ( ent->client->sess.spectatorClient >= 0 ) { + ent->client->sess.spectatorState = SPECTATOR_FREE; + } + ClientBegin( ent->client - level.clients ); } - - ClientBegin( ent->client - level.clients ); + else { + //freeze + if ( ent->client->sess.spectatorClient >= 0 ) { + StopFollowing( ent ); + } + //freeze + } + //qlone - optional FreezeTag } } @@ -1109,8 +1289,14 @@ */ void ClientEndFrame( gentity_t *ent ) { int i; - - if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + //unlagged - smooth clients #1 + int frames; + //unlagged - smooth clients #1 + + //freeze + //if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + if ( is_spectator( ent->client ) ) { + //freeze SpectatorClientEndFrame( ent ); return; } @@ -1163,26 +1349,70 @@ // apply all the damage taken this frame P_DamageFeedback (ent); + //unlagged - smooth clients #1 + // this is handled differently now + /* // add the EF_CONNECTION flag if we haven't gotten commands recently if ( level.time - ent->client->lastCmdTime > 1000 ) { ent->client->ps.eFlags |= EF_CONNECTION; } else { ent->client->ps.eFlags &= ~EF_CONNECTION; } + */ + //unlagged - smooth clients #1 ent->client->ps.stats[STAT_HEALTH] = ent->health; // FIXME: get rid of ent->health... G_SetClientSound (ent); // set the latest infor + //unlagged - smooth clients #2 + // clients no longer do extrapolation if cg_smoothClients is 1, because + // skip correction is all handled server-side now + // since that's the case, it makes no sense to store the extra info + // in the client's snapshot entity, so let's save a little bandwidth + /* if (g_smoothClients.integer) { BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); } else { + */ BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); - } + /*}*/ + //unlagged - smooth clients #2 + SendPendingPredictableEvents( &ent->client->ps ); + //unlagged - smooth clients #1 + // mark as not missing updates initially + ent->client->ps.eFlags &= ~EF_CONNECTION; + + // see how many frames the client has missed + frames = level.framenum - ent->client->lastUpdateFrame - 1; + + // don't extrapolate more than two frames + if ( frames > 2 ) { + frames = 2; + + // if they missed more than two in a row, show the phone jack + ent->client->ps.eFlags |= EF_CONNECTION; + ent->s.eFlags |= EF_CONNECTION; + } + + // did the client miss any frames? + if ( frames > 0 && g_smoothClients.integer ) { + // yep, missed one or more, so extrapolate the player's movement + G_PredictPlayerMove( ent, (float)frames / sv_fps.integer ); + // save network bandwidth + SnapVector( ent->s.pos.trBase ); + } + //unlagged - smooth clients #1 + + //unlagged - backward reconciliation #1 + // store the client's position for backward reconciliation later + G_StoreHistory( ent ); + //unlagged - backward reconciliation #1 + // set the bit for the reachability area the client is currently in // i = trap_AAS_PointReachabilityAreaIndex( ent->client->ps.origin ); // ent->client->areabits[i >> 3] |= 1 << (i & 7); diff -Nur ioq3/code/game/g_client.c qlone-mod/code/game/g_client.c --- ioq3/code/game/g_client.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/game/g_client.c 2020-05-06 11:16:21.000000000 +0200 @@ -529,6 +529,11 @@ */ void ClientRespawn( gentity_t *ent ) { + //freeze + //qlone - optional FreezeTag + if ( g_freezeTag.integer && Set_spectator( ent ) ) return; + //qlone - optional FreezeTag + //freeze CopyToBodyQue (ent); ClientSpawn(ent); } @@ -734,6 +739,41 @@ client->pers.predictItemPickup = qtrue; } + //unlagged - client options + // see if the player has opted out + s = Info_ValueForKey( userinfo, "cg_delag" ); + if ( !atoi( s ) ) { + client->pers.delag = 0; + } else { + client->pers.delag = atoi( s ); + } + + // see if the player is nudging his shots + s = Info_ValueForKey( userinfo, "cg_cmdTimeNudge" ); + client->pers.cmdTimeNudge = atoi( s ); + + // see if the player wants to debug the backward reconciliation + s = Info_ValueForKey( userinfo, "cg_debugDelag" ); + if ( !atoi( s ) ) { + client->pers.debugDelag = qfalse; + } + else { + client->pers.debugDelag = qtrue; + } + + // see if the player is simulating incoming latency + s = Info_ValueForKey( userinfo, "cg_latentSnaps" ); + client->pers.latentSnaps = atoi( s ); + + // see if the player is simulating outgoing latency + s = Info_ValueForKey( userinfo, "cg_latentCmds" ); + client->pers.latentCmds = atoi( s ); + + // see if the player is simulating outgoing packet loss + s = Info_ValueForKey( userinfo, "cg_plOut" ); + client->pers.plOut = atoi( s ); + //unlagged - client options + // set name Q_strncpyz ( oldname, client->pers.netname, sizeof( oldname ) ); s = Info_ValueForKey (userinfo, "name"); @@ -954,6 +994,18 @@ } G_ReadSessionData( client ); + //qlone - optional FreezeTag + if (g_freezeTag.integer) { + //freeze + if ( g_gametype.integer != GT_TOURNAMENT ) { + client->sess.wins = 0; + } + ent->freezeState = qfalse; + ent->readyBegin = qfalse; + //freeze + } + //qlone - optional FreezeTag + // get and distribute relevant parameters G_LogPrintf( "ClientConnect: %i\n", clientNum ); ClientUserinfoChanged( clientNum ); @@ -976,6 +1028,23 @@ // if ( !client->areabits ) // client->areabits = G_Alloc( (trap_AAS_PointReachabilityAreaIndex( NULL ) + 7) / 8 ); + //unlagged - backward reconciliation #5 + // announce it + if ( g_delagHitscan.integer ) { + trap_SendServerCommand( clientNum, "print \"This server is Unlagged: full lag compensation is ON!\n\"" ); + } + else { + trap_SendServerCommand( clientNum, "print \"This server is Unlagged: full lag compensation is OFF!\n\"" ); + } + //unlagged - backward reconciliation #5 + + //qlone - optional FreezeTag + // announce if the FreezeTag variant is enabled + if ( g_freezeTag.integer ) { + trap_SendServerCommand( clientNum, "print \"This game has FreezeTag variant enabled\n\"" ); + } + //qlone - optional FreezeTag + return NULL; } @@ -1057,6 +1126,9 @@ int accuracy_hits, accuracy_shots; int eventSequence; char userinfo[MAX_INFO_STRING]; + //qlone + int health; + //qlone index = ent - g_entities; client = ent->client; @@ -1104,6 +1176,13 @@ flags = ent->client->ps.eFlags & (EF_TELEPORT_BIT | EF_VOTED | EF_TEAMVOTED); flags ^= EF_TELEPORT_BIT; + //unlagged - backward reconciliation #3 + // we don't want players being backward-reconciled to the place they died + G_ResetHistory( ent ); + // and this is as good a time as any to clear the saved state + ent->client->saved.leveltime = 0; + //unlagged - backward reconciliation #3 + // clear everything but the persistant data saved = client->pers; @@ -1175,8 +1254,26 @@ client->ps.ammo[WP_GAUNTLET] = -1; client->ps.ammo[WP_GRAPPLING_HOOK] = -1; - // health will count down towards max_health - ent->health = client->ps.stats[STAT_HEALTH] = client->ps.stats[STAT_MAX_HEALTH] + 25; + //qlone - instaGib + // only the railgun will be available + if (g_instaGib.integer) { + client->ps.stats[STAT_WEAPONS] = ( 1 << WP_RAILGUN ); + client->ps.ammo[WP_RAILGUN] = INFINITE; + } + //qlone - instaGib + + //qlone + if (!g_startHealth.integer) { + //qlone + // health will count down towards max_health + ent->health = client->ps.stats[STAT_HEALTH] = client->ps.stats[STAT_MAX_HEALTH] + 25; + //qlone + } + else { + health = g_startHealth.integer > 200 ? 200 : g_startHealth.integer; + ent->health = client->ps.stats[STAT_HEALTH] = health; + } + //qlone G_SetOrigin( ent, spawn_origin ); VectorCopy( spawn_origin, client->ps.origin ); @@ -1199,13 +1296,33 @@ client->ps.legsAnim = LEGS_IDLE; if (!level.intermissiontime) { - if (ent->client->sess.sessionTeam != TEAM_SPECTATOR) { + //freeze + //if (ent->client->sess.sessionTeam != TEAM_SPECTATOR) { + if ( !is_spectator( client ) ) { + //freeze G_KillBox(ent); // force the base weapon up client->ps.weapon = WP_MACHINEGUN; client->ps.weaponstate = WEAPON_READY; + //freeze + //qlone - optional FreezeTag + if (g_freezeTag.integer) { + SpawnWeapon( client ); + } + //qlone - optional FreezeTag + // fire the targets of the spawn point - G_UseTargets(spawnPoint, ent); + //qlone - optional FreezeTag + if (!g_freezeTag.integer) { + G_UseTargets(spawnPoint, ent); + } + else { + //freeze + if ( !( g_dmflags.integer & 1024 ) ) + G_UseTargets(spawnPoint, ent); + //freeze + } + //qlone - optional FreezeTag // select the highest weapon number available, after any spawn given items have fired client->ps.weapon = 1; @@ -1215,6 +1332,26 @@ break; } } + + //freeze + //qlone - optional FreezeTag + if (g_freezeTag.integer) { + if ( client->ps.stats[ STAT_WEAPONS ] & ( 1 << WP_ROCKET_LAUNCHER ) ) { + client->ps.weapon = WP_ROCKET_LAUNCHER; + } + } + //qlone - optional FreezeTag + //freeze + + //qlone - was only for FreezeTag, made it avail for all + if ( g_startArmor.integer > 0 ) { + client->ps.stats[ STAT_ARMOR ] += g_startArmor.integer; + if ( client->ps.stats[ STAT_ARMOR ] > client->ps.stats[ STAT_MAX_HEALTH ] * 2 ) { + client->ps.stats[ STAT_ARMOR ] = client->ps.stats[ STAT_MAX_HEALTH ] * 2; + } + } + //qlone - was only for FreezeTag, made it avail for all + // positively link the client, even if the command times are weird VectorCopy(ent->client->ps.origin, ent->r.currentOrigin); @@ -1271,7 +1408,10 @@ // stop any following clients for ( i = 0 ; i < level.maxclients ; i++ ) { - if ( level.clients[i].sess.sessionTeam == TEAM_SPECTATOR + //freeze + //if ( level.clients[i].sess.sessionTeam == TEAM_SPECTATOR + if ( is_spectator( &level.clients[ i ] ) + //freeze && level.clients[i].sess.spectatorState == SPECTATOR_FOLLOW && level.clients[i].sess.spectatorClient == clientNum ) { StopFollowing( &g_entities[i] ); @@ -1280,7 +1420,10 @@ // send effect if they were completely connected if ( ent->client->pers.connected == CON_CONNECTED - && ent->client->sess.sessionTeam != TEAM_SPECTATOR ) { + //freeze + //&& ent->client->sess.sessionTeam != TEAM_SPECTATOR ) { + && !is_spectator( ent->client ) ) { + //freeze tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_OUT ); tent->s.clientNum = ent->s.clientNum; diff -Nur ioq3/code/game/g_cmds.c qlone-mod/code/game/g_cmds.c --- ioq3/code/game/g_cmds.c 2020-04-10 15:55:45.000000000 +0200 +++ qlone-mod/code/game/g_cmds.c 2020-04-27 13:32:45.000000000 +0200 @@ -60,7 +60,10 @@ if ( cl->pers.connected == CON_CONNECTING ) { ping = -1; } else { - ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + //unlagged - true ping + //ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + ping = cl->pers.realPing < 999 ? cl->pers.realPing : 999; + //unlagged - true ping } if( cl->accuracy_shots ) { @@ -70,6 +73,13 @@ accuracy = 0; } perfect = ( cl->ps.persistant[PERS_RANK] == 0 && cl->ps.persistant[PERS_KILLED] == 0 ) ? 1 : 0; + //freeze + //qlone - optional FreezeTag + if (g_freezeTag.integer) { + scoreFlags = cl->sess.wins; + } + //qlone - optional FreezeTag + //freeze Com_sprintf (entry, sizeof(entry), " %i %i %i %i %i %i %i %i %i %i %i %i %i %i", level.sortedClients[i], @@ -472,7 +482,10 @@ ================= */ void Cmd_Kill_f( gentity_t *ent ) { - if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + //freeze + //if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + if ( is_spectator( ent->client ) ) { + //freeze return; } if (ent->health <= 0) { @@ -480,7 +493,16 @@ } ent->flags &= ~FL_GODMODE; ent->client->ps.stats[STAT_HEALTH] = ent->health = -999; - player_die (ent, ent, ent, 100000, MOD_SUICIDE); + //qlone - optional FreezeTag + if (!g_freezeTag.integer) { + player_die (ent, ent, ent, 100000, MOD_SUICIDE); + } + else { + //freeze + player_die (ent, ent, ent, 100000, MOD_BFG_SPLASH); + //freeze + } + //qlone - optional FreezeTag } /* @@ -660,7 +682,17 @@ */ void StopFollowing( gentity_t *ent ) { ent->client->ps.persistant[ PERS_TEAM ] = TEAM_SPECTATOR; - ent->client->sess.sessionTeam = TEAM_SPECTATOR; + //qlone - optional FreezeTag + if (!g_freezeTag.integer) { + ent->client->sess.sessionTeam = TEAM_SPECTATOR; + } + else { + //freeze + SetClientViewAngle( ent, ent->client->ps.viewangles ); + ent->client->ps.stats[ STAT_HEALTH ] = ent->health = 100; + memset( ent->client->ps.powerups, 0, sizeof ( ent->client->ps.powerups ) ); + //freeze + } ent->client->sess.spectatorState = SPECTATOR_FREE; ent->client->ps.pm_flags &= ~PMF_FOLLOW; ent->r.svFlags &= ~SVF_BOT; @@ -713,6 +745,17 @@ ent->client->sess.losses++; } + //freeze + //qlone - optional FreezeTag + if ( g_freezeTag.integer && ent->freezeState ) { + //qlone - optional FreezeTag + if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) { + StopFollowing( ent ); + } + return; + } + //freeze + trap_Argv( 1, s, sizeof( s ) ); SetTeam( ent, s ); @@ -749,9 +792,21 @@ } // can't follow another spectator - if ( level.clients[ i ].sess.sessionTeam == TEAM_SPECTATOR ) { - return; + //qlone - optional FreezeTag + if (!g_freezeTag.integer) { + if ( level.clients[ i ].sess.sessionTeam == TEAM_SPECTATOR ) { + return; + } + } + else { + //freeze + if ( ent->freezeState && !is_spectator( ent->client ) ) return; + if ( is_spectator( &level.clients[ i ] ) ) { + return; + } + //freeze } + //qlone - optional FreezeTag // if they are playing a tournement game, count as a loss if ( (g_gametype.integer == GT_TOURNAMENT ) @@ -777,6 +832,15 @@ int clientnum; int original; + //qlone - optional FreezeTag + if (g_freezeTag.integer) { + //freeze + if ( ent->freezeState && !is_spectator( ent->client ) ) return; + if ( Set_Client( ent ) ) return; + //freeze + } + //qlone - optional FreezeTag + // if they are playing a tournement game, count as a loss if ( (g_gametype.integer == GT_TOURNAMENT ) && ent->client->sess.sessionTeam == TEAM_FREE ) { @@ -817,10 +881,38 @@ continue; } - // can't follow another spectator - if ( level.clients[ clientnum ].sess.sessionTeam == TEAM_SPECTATOR ) { - continue; + //qlone - optional FreezeTag + if (!g_freezeTag.integer) { + // can't follow another spectator + if ( level.clients[ clientnum ].sess.sessionTeam == TEAM_SPECTATOR ) { + continue; + } } + else { + //freeze + if ( &level.clients[ clientnum ] == ent->client ) { + if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) { + StopFollowing( ent ); + ent->client->ps.pm_flags |= PMF_TIME_KNOCKBACK; + ent->client->ps.pm_time = 100; + return; + } + } + if ( g_entities[ clientnum ].freezeState ) continue; + if ( is_spectator( &level.clients[ clientnum ] ) ) { + continue; + } + //freeze + + //qlone - limit spectator to teammates + if (g_specLock.integer + && ( (ent->client->sess.sessionTeam == TEAM_RED && level.clients[ clientnum ].sess.sessionTeam == TEAM_BLUE) + || (ent->client->sess.sessionTeam == TEAM_BLUE && level.clients[ clientnum ].sess.sessionTeam == TEAM_RED) ) ) { + continue; + } + //qlone - limit spectator to teammates + } + //qlone - optional FreezeTag // this is good, we can use it ent->client->sess.spectatorClient = clientnum; @@ -1336,11 +1428,29 @@ } else if ( !Q_stricmp( arg1, "g_doWarmup" ) ) { } else if ( !Q_stricmp( arg1, "timelimit" ) ) { } else if ( !Q_stricmp( arg1, "fraglimit" ) ) { + //freeze + } else if ( !Q_stricmp( arg1, "g_grapple" ) ) { + //freeze } else { + //freeze + if ( g_votelimit.integer ) { + voteInvalid( ent ); + } else { + //freeze trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string.\n\"" ); trap_SendServerCommand( ent-g_entities, "print \"Vote commands are: map_restart, nextmap, map , g_gametype , kick , clientkick , g_doWarmup, timelimit