From 62f995db7c23e9762ce2c26f61f58bd4ee1b905b Mon Sep 17 00:00:00 2001 From: ec- Date: Wed, 2 Aug 2017 16:57:45 +0300 Subject: [PATCH] Add optimized client-side prediction Locally predict weapon/ammo/armor/health/powerups pickups Fix picked up items appearing for a fraction of second right after pickup Delay item pickup after respawn Filter jump/pain events to avoid duplicates/sound distortion Try to play potentially dropped events Print ping in top-left corner of lagometer, fix "snc" label position Code cleanup --- code/cgame/cg_draw.c | 34 +- code/cgame/cg_ents.c | 4 +- code/cgame/cg_event.c | 53 ++- code/cgame/cg_local.h | 27 +- code/cgame/cg_marks.c | 8 +- code/cgame/cg_playerstate.c | 69 ++-- code/cgame/cg_predict.c | 671 ++++++++++++++++++++++++++++++++++-- code/cgame/cg_view.c | 2 - code/game/bg_misc.c | 13 +- code/game/bg_pmove.c | 7 +- code/game/bg_public.h | 8 +- code/game/g_active.c | 9 +- code/game/g_client.c | 23 +- code/game/g_combat.c | 15 +- code/game/g_items.c | 22 +- code/game/g_utils.c | 2 +- 16 files changed, 833 insertions(+), 134 deletions(-) diff --git a/code/cgame/cg_draw.c b/code/cgame/cg_draw.c index 8c46e17e..87973614 100644 --- a/code/cgame/cg_draw.c +++ b/code/cgame/cg_draw.c @@ -1595,6 +1595,7 @@ void CG_AddLagometerFrameInfo( void ) { lagometer.frameCount++; } + /* ============== CG_AddLagometerSnapshotInfo @@ -1619,6 +1620,7 @@ void CG_AddLagometerSnapshotInfo( snapshot_t *snap ) { lagometer.snapshotCount++; } + /* ============== CG_DrawDisconnect @@ -1767,7 +1769,11 @@ static void CG_DrawLagometer( void ) { trap_R_SetColor( NULL ); if ( cg_nopredict.integer || cg_synchronousClients.integer ) { - CG_DrawBigString( ax, ay, "snc", 1.0 ); + CG_DrawStringExt( 640 - 16, y, "snc", g_color_table[ColorIndex(COLOR_WHITE)], qfalse, qfalse, 5, 10, 0 ); + } + + if ( !cg.demoPlayback ) { + CG_DrawStringExt( x+1, y, va( "%ims", cg.meanPing ), g_color_table[ColorIndex(COLOR_WHITE)], qfalse, qfalse, 5, 10, 0 ); } CG_DrawDisconnect(); @@ -2578,6 +2584,28 @@ static void CG_DrawTourneyScoreboard( void ) { #endif } + +static void CG_CalculatePing( void ) { + int count, i, v; + + cg.meanPing = 0; + + for ( i = 0, count = 0; i < LAG_SAMPLES; i++ ) { + + v = lagometer.snapshotSamples[i]; + if ( v >= 0 ) { + cg.meanPing += v; + count++; + } + + } + + if ( count ) { + cg.meanPing /= count; + } +} + + /* ===================== CG_DrawActive @@ -2592,6 +2620,10 @@ void CG_DrawActive( stereoFrame_t stereoView ) { return; } + if ( !cg.demoPlayback ) { + CG_CalculatePing(); + } + // optionally draw the tournement scoreboard instead if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_SPECTATOR && ( cg.snap->ps.pm_flags & PMF_SCOREBOARD ) ) { diff --git a/code/cgame/cg_ents.c b/code/cgame/cg_ents.c index 8c660ed2..cd3b2ef7 100644 --- a/code/cgame/cg_ents.c +++ b/code/cgame/cg_ents.c @@ -207,7 +207,7 @@ CG_Item static void CG_Item( centity_t *cent ) { refEntity_t ent; entityState_t *es; - gitem_t *item; + const gitem_t *item; int msec; float frac; float scale; @@ -221,7 +221,7 @@ static void CG_Item( centity_t *cent ) { } // if set to invisible, skip - if ( !es->modelindex || ( es->eFlags & EF_NODRAW ) ) { + if ( !es->modelindex || ( es->eFlags & EF_NODRAW ) || cent->delaySpawn > cg.time ) { return; } diff --git a/code/cgame/cg_event.c b/code/cgame/cg_event.c index be7df070..d3c9a1e2 100644 --- a/code/cgame/cg_event.c +++ b/code/cgame/cg_event.c @@ -375,9 +375,9 @@ static void CG_UseItem( centity_t *cent ) { break; #endif } - } + /* ================ CG_ItemPickup @@ -464,6 +464,12 @@ void CG_PainEvent( centity_t *cent, int health ) { // don't do more than two pain sounds a second if ( cg.time - cent->pe.painTime < 500 ) { + cent->pe.painIgnore = qfalse; + return; + } + + if ( cent->pe.painIgnore ) { + cent->pe.painIgnore = qfalse; return; } @@ -504,15 +510,16 @@ also called by CG_CheckPlayerstateEvents ============== */ #define DEBUGNAME(x) if(cg_debugEvents.integer){CG_Printf(x"\n");} -void CG_EntityEvent( centity_t *cent, vec3_t position ) { +void CG_EntityEvent( centity_t *cent, vec3_t position, int entityNum ) { entityState_t *es; - int event; + entity_event_t event; vec3_t dir; const char *s; int clientNum; clientInfo_t *ci; vec3_t vec; float fovOffset; + centity_t *ce; es = ¢->currentState; event = es->event & ~EV_EVENT_BITS; @@ -586,6 +593,8 @@ void CG_EntityEvent( centity_t *cent, vec3_t position ) { DEBUGNAME("EV_FALL_MEDIUM"); // use normal pain sound trap_S_StartSound( NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*pain100_1.wav" ) ); + cent->pe.painIgnore = qtrue; + cent->pe.painTime = cg.time; // don't play a pain sound right after this if ( clientNum == cg.predictedPlayerState.clientNum ) { // smooth landing z changes cg.landChange = -16; @@ -595,6 +604,7 @@ void CG_EntityEvent( centity_t *cent, vec3_t position ) { case EV_FALL_FAR: DEBUGNAME("EV_FALL_FAR"); trap_S_StartSound (NULL, es->number, CHAN_AUTO, CG_CustomSound( es->number, "*fall1.wav" ) ); + cent->pe.painIgnore = qtrue; cent->pe.painTime = cg.time; // don't play a pain sound right after this if ( clientNum == cg.predictedPlayerState.clientNum ) { // smooth landing z changes @@ -662,7 +672,9 @@ void CG_EntityEvent( centity_t *cent, vec3_t position ) { case EV_JUMP: DEBUGNAME("EV_JUMP"); - trap_S_StartSound (NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*jump1.wav" ) ); + // pain event with fast sequential jump just creates sound distortion + if ( cg.time - cent->pe.painTime > 50 ) + trap_S_StartSound (NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*jump1.wav" ) ); break; case EV_TAUNT: DEBUGNAME("EV_TAUNT"); @@ -722,6 +734,17 @@ void CG_EntityEvent( centity_t *cent, vec3_t position ) { if ( index < 1 || index >= bg_numItems ) { break; } + + if ( entityNum >= 0 ) { + // our predicted entity + ce = cg_entities + entityNum; + if ( ce->delaySpawn > cg.time && ce->delaySpawnPlayed ) { + break; // delay item pickup + } + } else { + ce = NULL; + } + item = &bg_itemlist[ index ]; // powerups and team items will have a separate global sound, this one @@ -753,6 +776,10 @@ void CG_EntityEvent( centity_t *cent, vec3_t position ) { if ( es->number == cg.snap->ps.clientNum ) { CG_ItemPickup( index ); } + + if ( ce ) { + ce->delaySpawnPlayed = qtrue; + } } break; @@ -767,6 +794,17 @@ void CG_EntityEvent( centity_t *cent, vec3_t position ) { if ( index < 1 || index >= bg_numItems ) { break; } + + if ( entityNum >= 0 ) { + // our predicted entity + ce = cg_entities + entityNum; + if ( ce->delaySpawn > cg.time && ce->delaySpawnPlayed ) { + break; + } + } else { + ce = NULL; + } + item = &bg_itemlist[ index ]; // powerup pickups are global if( item->pickup_sound ) { @@ -777,6 +815,10 @@ void CG_EntityEvent( centity_t *cent, vec3_t position ) { if ( es->number == cg.snap->ps.clientNum ) { CG_ItemPickup( index ); } + + if ( ce ) { + ce->delaySpawnPlayed = qtrue; + } } break; @@ -1277,6 +1319,5 @@ void CG_CheckEvents( centity_t *cent ) { BG_EvaluateTrajectory( ¢->currentState.pos, cg.snap->serverTime, cent->lerpOrigin ); CG_SetEntitySoundPosition( cent ); - CG_EntityEvent( cent, cent->lerpOrigin ); + CG_EntityEvent( cent, cent->lerpOrigin, -1 ); } - diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index 55df2d38..54b4fcbb 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -146,6 +146,7 @@ typedef struct { lerpFrame_t legs, torso, flag; int painTime; int painDirection; // flip from 0 to 1 + qboolean painIgnore; int lightningFiring; // railgun trail spawning @@ -177,6 +178,8 @@ typedef struct centity_s { int trailTime; // so missile trails can handle dropped initial packets int dustTrailTime; int miscTime; + int delaySpawn; + qboolean delaySpawnPlayed; int snapShotTime; // last time this entity was found in a snapshot @@ -446,7 +449,11 @@ typedef struct { // occurs, and they will have visible effects for #define STEP_TIME or whatever msec after #define MAX_PREDICTED_EVENTS 16 - + +#define PICKUP_PREDICTION_DELAY 200 + +#define NUM_SAVED_STATES ( CMD_BACKUP + 2 ) + typedef struct { int clientFrame; // incremented each frame @@ -648,6 +655,15 @@ typedef struct { char testModelName[MAX_QPATH]; qboolean testGun; + // optimized prediction + int lastPredictedCommand; + int lastServerTime; + playerState_t savedPmoveStates[ NUM_SAVED_STATES ]; + int stateHead, stateTail; + + int meanPing; + int timeResidual; + int allowPickupPrediction; } cg_t; @@ -875,7 +891,6 @@ typedef struct { sfxHandle_t respawnSound; sfxHandle_t talkSound; sfxHandle_t landSound; - sfxHandle_t fallSound; sfxHandle_t jumpPadSound; sfxHandle_t oneMinuteSound; @@ -976,8 +991,6 @@ typedef struct { sfxHandle_t scoutSound; #endif qhandle_t cursor; - qhandle_t selectCursor; - qhandle_t sizeCursor; sfxHandle_t regenSound; sfxHandle_t protectSound; @@ -1198,6 +1211,7 @@ extern vmCvar_t cg_recordSPDemo; extern vmCvar_t cg_recordSPDemoName; extern vmCvar_t cg_obeliskRespawnDelay; #endif +extern const char *eventnames[EV_MAX]; // // cg_main.c @@ -1334,13 +1348,14 @@ void CG_Trace( trace_t *result, const vec3_t start, const vec3_t mins, const vec void CG_PredictPlayerState( void ); void CG_LoadDeferredPlayers( void ); +void CG_PlayDroppedEvents( playerState_t *ps, playerState_t *ops ); // // cg_events.c // void CG_CheckEvents( centity_t *cent ); -const char *CG_PlaceString( int rank ); -void CG_EntityEvent( centity_t *cent, vec3_t position ); +const char *CG_PlaceString( int rank ); +void CG_EntityEvent( centity_t *cent, vec3_t position, int entityNum ); void CG_PainEvent( centity_t *cent, int health ); diff --git a/code/cgame/cg_marks.c b/code/cgame/cg_marks.c index 880bec0e..ca8c4049 100644 --- a/code/cgame/cg_marks.c +++ b/code/cgame/cg_marks.c @@ -362,11 +362,11 @@ cparticle_t *active_particles, *free_particles; cparticle_t particles[MAX_PARTICLES]; const int cl_numparticles = MAX_PARTICLES; -qboolean initparticles = qfalse; -vec3_t pvforward, pvright, pvup; -vec3_t rforward, rright, rup; +qboolean initparticles = qfalse; +vec3_t pvforward, pvright, pvup; +vec3_t rforward, rright, rup; -float oldtime; +int oldtime; /* =============== diff --git a/code/cgame/cg_playerstate.c b/code/cgame/cg_playerstate.c index d347df89..7fb56bb6 100644 --- a/code/cgame/cg_playerstate.c +++ b/code/cgame/cg_playerstate.c @@ -190,17 +190,21 @@ void CG_Respawn( void ) { // select the weapon the server says we are using cg.weaponSelect = cg.snap->ps.weapon; + + cg.timeResidual = cg.snap->ps.commandTime + 1000; } -extern char *eventnames[]; /* ============== CG_CheckPlayerstateEvents ============== */ -void CG_CheckPlayerstateEvents( playerState_t *ps, playerState_t *ops ) { - int i; +extern int eventStack; +extern int eventParm2[ MAX_PREDICTED_EVENTS ]; + +static void CG_CheckPlayerstateEvents( const playerState_t *ps, const playerState_t *ops ) { + int i, n; int event; centity_t *cent; @@ -208,10 +212,12 @@ void CG_CheckPlayerstateEvents( playerState_t *ps, playerState_t *ops ) { cent = &cg_entities[ ps->clientNum ]; cent->currentState.event = ps->externalEvent; cent->currentState.eventParm = ps->externalEventParm; - CG_EntityEvent( cent, cent->lerpOrigin ); + CG_EntityEvent( cent, cent->lerpOrigin, -1 ); } cent = &cg.predictedPlayerEntity; // cg_entities[ ps->clientNum ]; + n = eventStack - MAX_PS_EVENTS; + if ( n < 0 ) n = 0; // go through the predictable events buffer for ( i = ps->eventSequence - MAX_PS_EVENTS ; i < ps->eventSequence ; i++ ) { // if we have a new predictable event @@ -225,7 +231,8 @@ void CG_CheckPlayerstateEvents( playerState_t *ps, playerState_t *ops ) { continue; cent->currentState.event = event; cent->currentState.eventParm = ps->eventParms[ i & (MAX_PS_EVENTS-1) ]; - CG_EntityEvent( cent, cent->lerpOrigin ); + + CG_EntityEvent( cent, cent->lerpOrigin, eventParm2[ n++ ] ); cg.predictableEvents[ i & (MAX_PREDICTED_EVENTS-1) ] = event; @@ -234,49 +241,14 @@ void CG_CheckPlayerstateEvents( playerState_t *ps, playerState_t *ops ) { } } -/* -================== -CG_CheckChangedPredictableEvents -================== -*/ -void CG_CheckChangedPredictableEvents( playerState_t *ps ) { - int i; - int event; - centity_t *cent; - - cent = &cg.predictedPlayerEntity; - for ( i = ps->eventSequence - MAX_PS_EVENTS ; i < ps->eventSequence ; i++ ) { - // - if (i >= cg.eventSequence) { - continue; - } - // if this event is not further back in than the maximum predictable events we remember - if (i > cg.eventSequence - MAX_PREDICTED_EVENTS) { - // if the new playerstate event is different from a previously predicted one - if ( ps->events[i & (MAX_PS_EVENTS-1)] != cg.predictableEvents[i & (MAX_PREDICTED_EVENTS-1) ] ) { - - event = ps->events[ i & (MAX_PS_EVENTS-1) ]; - cent->currentState.event = event; - cent->currentState.eventParm = ps->eventParms[ i & (MAX_PS_EVENTS-1) ]; - CG_EntityEvent( cent, cent->lerpOrigin ); - - cg.predictableEvents[ i & (MAX_PREDICTED_EVENTS-1) ] = event; - - if ( cg_showmiss.integer ) { - CG_Printf("WARNING: changed predicted event\n"); - } - } - } - } -} /* ================== pushReward ================== */ -static void pushReward(sfxHandle_t sfx, qhandle_t shader, int rewardCount) { - if (cg.rewardStack < (MAX_REWARDSTACK-1)) { +static void pushReward( sfxHandle_t sfx, qhandle_t shader, int rewardCount ) { + if ( cg.rewardStack < (MAX_REWARDSTACK-1 )) { cg.rewardStack++; cg.rewardSound[cg.rewardStack] = sfx; cg.rewardShader[cg.rewardStack] = shader; @@ -284,6 +256,7 @@ static void pushReward(sfxHandle_t sfx, qhandle_t shader, int rewardCount) { } } + /* ================== CG_CheckLocalSounds @@ -438,7 +411,7 @@ void CG_CheckLocalSounds( playerState_t *ps, playerState_t *ops ) { } // timelimit warnings - if ( cgs.timelimit > 0 ) { + if ( cgs.timelimit > 0 && !cg.warmup ) { int msec; msec = cg.time - cgs.levelStartTime; @@ -448,11 +421,11 @@ void CG_CheckLocalSounds( playerState_t *ps, playerState_t *ops ) { } else if ( !( cg.timelimitWarnings & 2 ) && msec > (cgs.timelimit - 1) * 60 * 1000 ) { cg.timelimitWarnings |= 1 | 2; - trap_S_StartLocalSound( cgs.media.oneMinuteSound, CHAN_ANNOUNCER ); + CG_AddBufferedSound( cgs.media.oneMinuteSound ); } else if ( cgs.timelimit > 5 && !( cg.timelimitWarnings & 1 ) && msec > (cgs.timelimit - 5) * 60 * 1000 ) { cg.timelimitWarnings |= 1; - trap_S_StartLocalSound( cgs.media.fiveMinuteSound, CHAN_ANNOUNCER ); + CG_AddBufferedSound( cgs.media.fiveMinuteSound ); } } @@ -515,9 +488,15 @@ void CG_TransitionPlayerState( playerState_t *ps, playerState_t *ops ) { // check for going low on ammo CG_CheckAmmo(); + // try to play potentially dropped events + CG_PlayDroppedEvents( ps, ops ); + // run events CG_CheckPlayerstateEvents( ps, ops ); + // reset event stack + eventStack = 0; + // smooth the ducking viewheight change if ( ps->viewheight != ops->viewheight && !respawn ) { cg.duckChange = ps->viewheight - ops->viewheight; diff --git a/code/cgame/cg_predict.c b/code/cgame/cg_predict.c index 78e2b7c7..7657b14a 100644 --- a/code/cgame/cg_predict.c +++ b/code/cgame/cg_predict.c @@ -56,6 +56,7 @@ void CG_BuildSolidList( void ) { } } + /* ==================== CG_ClipMoveToEntities @@ -117,6 +118,7 @@ static void CG_ClipMoveToEntities ( const vec3_t start, const vec3_t mins, const } } + /* ================ CG_Trace @@ -131,12 +133,14 @@ void CG_Trace( trace_t *result, const vec3_t start, const vec3_t mins, const vec t.entityNum = ENTITYNUM_NONE; else t.entityNum = ENTITYNUM_WORLD; + // check all other solid models CG_ClipMoveToEntities (start, mins, maxs, end, skipNumber, mask, &t); *result = t; } + /* ================ CG_PointContents @@ -236,6 +240,229 @@ static void CG_InterpolatePlayerState( qboolean grabAngles ) { } +int eventStack; +entity_event_t events[ MAX_PREDICTED_EVENTS ]; +int eventParms[ MAX_PREDICTED_EVENTS ]; +int eventParm2[ MAX_PREDICTED_EVENTS ]; // client entity index + +void CG_AddFallDamage( int damage ); + +/* +=================== +CG_StoreEvents + +Save events that may be dropped during prediction +=================== +*/ +void CG_StoreEvent( entity_event_t evt, int eventParm, int entityNum ) +{ + if ( eventStack >= MAX_PREDICTED_EVENTS ) + return; + + if ( evt == EV_FALL_FAR ) { + CG_AddFallDamage( 10 ); + } else if ( evt == EV_FALL_MEDIUM ) { + CG_AddFallDamage( 5 ); + } + + events[ eventStack ] = evt; + eventParms[ eventStack ] = eventParm; + eventParm2[ eventStack ] = entityNum; + eventStack++; +} + + +/* +=================== +CG_PlayDroppedEvents +=================== +*/ +void CG_PlayDroppedEvents( playerState_t *ps, playerState_t *ops ) { + centity_t *cent; + entity_event_t oldEvent; + int i, oldParam; + + if ( ps == ops ) { + return; + } + + if ( eventStack <= MAX_PS_EVENTS ) { + return; + } + + cent = &cg.predictedPlayerEntity; + + oldEvent = cent->currentState.event; + oldParam = cent->currentState.eventParm; + + for ( i = 0; i < eventStack - MAX_PS_EVENTS ; i++ ) { + cent->currentState.event = events[ i ]; + cent->currentState.eventParm = eventParms[ i ]; + if ( cg_showmiss.integer ) + { + CG_Printf( "Playing dropped event: %s %i", eventnames[ events[ i ] ], eventParms[ i ] ); + } + CG_EntityEvent( cent, cent->lerpOrigin, eventParm2[ i ] ); + cg.eventSequence++; + } + + cent->currentState.event = oldEvent; + cent->currentState.eventParm = oldParam; +} + + +static void CG_AddArmor( const gitem_t *item, int quantity ) { + + cg.predictedPlayerState.stats[STAT_ARMOR] += quantity; + + if ( cg.predictedPlayerState.stats[STAT_ARMOR] > cg.predictedPlayerState.stats[STAT_MAX_HEALTH]*2 ) + cg.predictedPlayerState.stats[STAT_ARMOR] = cg.predictedPlayerState.stats[STAT_MAX_HEALTH]*2; +} + + +static void CG_AddAmmo( int weapon, int count ) +{ + if ( weapon == WP_GAUNTLET || weapon == WP_GRAPPLING_HOOK ) { + cg.predictedPlayerState.ammo[weapon] = -1; + } else { + cg.predictedPlayerState.ammo[weapon] += count; + if ( weapon >= WP_MACHINEGUN && weapon <= WP_BFG ) { + if ( cg.predictedPlayerState.ammo[weapon] > AMMO_HARD_LIMIT ) { + cg.predictedPlayerState.ammo[weapon] = AMMO_HARD_LIMIT; + } + } + } +} + + +static void CG_AddWeapon( int weapon, int quantity, qboolean dropped ) +{ + int ammo; + + ammo = quantity; + + // dropped items and teamplay weapons always have full ammo + if ( !dropped && cgs.gametype != GT_TEAM ) { + if ( cg.predictedPlayerState.ammo[ weapon ] < quantity ) { + quantity = quantity - cg.predictedPlayerState.ammo[ weapon ]; + } else { + quantity = 1; + } + } + + // add the weapon + cg.predictedPlayerState.stats[STAT_WEAPONS] |= ( 1 << weapon ); + + CG_AddAmmo( weapon, quantity ); +} + + +static int CG_CheckArmor( int damage ) { + int save; + int count; + + count = cg.predictedPlayerState.stats[STAT_ARMOR]; + + save = ceil( damage * ARMOR_PROTECTION ); + + if (save >= count) + save = count; + + if ( !save ) + return 0; + + cg.predictedPlayerState.stats[STAT_ARMOR] -= save; + + return save; +} + + + void CG_AddFallDamage( int damage ) +{ + int take, asave; + + if ( cg.predictedPlayerState.powerups[ PW_BATTLESUIT ] ) + return; + + if ( cg.predictedPlayerState.clientNum != cg.snap->ps.clientNum || cg.snap->ps.pm_flags & PMF_FOLLOW ) { + return; + } + + take = damage; + + asave = CG_CheckArmor( take ); + + take -= asave; + + cg.predictedPlayerState.stats[STAT_HEALTH] -= take; + +#if 0 + CG_Printf( "take: %i asave:%i health:%i armor:%i\n", take, asave, + cg.predictedPlayerState.stats[STAT_HEALTH], cg.predictedPlayerState.stats[STAT_ARMOR] ); +#endif + + cg.predictedPlayerState.damagePitch = 255; + cg.predictedPlayerState.damageYaw = 255; + //cg.predictedPlayerState.damageEvent++; + cg.predictedPlayerState.damageCount = take + asave; +} + + +static void CG_PickupPrediction( centity_t *cent, const gitem_t *item ) { + + // health prediction + if ( item->giType == IT_HEALTH && cent->currentState.time2 > 0 ) { + int limit; + + limit = cg.predictedPlayerState.stats[ STAT_MAX_HEALTH ]; // soft limit + if ( !Q_stricmp( item->classname, "item_health_small" ) || !Q_stricmp( item->classname, "item_health_mega" ) ) { + limit *= 2; // hard limit + } + + cg.predictedPlayerState.stats[STAT_HEALTH] += cent->currentState.time2; + if ( cg.predictedPlayerState.stats[ STAT_HEALTH ] > limit ) { + cg.predictedPlayerState.stats[ STAT_HEALTH ] = limit; + } + } + + // armor prediction + if ( item->giType == IT_ARMOR && cent->currentState.time2 > 0 ) { + CG_AddArmor( item, cent->currentState.time2 ); + return; + } + + // ammo prediction + if ( item->giType == IT_AMMO && cent->currentState.time2 > 0 ) { + CG_AddAmmo( item->giTag, cent->currentState.time2 ); + return; + } + + // weapon prediction + if ( item->giType == IT_WEAPON && cent->currentState.time2 > 0 ) { + CG_AddWeapon( item->giTag, cent->currentState.time2, (cent->currentState.modelindex2 == 1) ); + return; + } + + // powerups prediction + if ( item->giType == IT_POWERUP && item->giTag >= PW_QUAD && item->giTag <= PW_FLIGHT ) { + // round timing to seconds to make multiple powerup timers count in sync + if ( !cg.predictedPlayerState.powerups[ item->giTag ] ) { + cg.predictedPlayerState.powerups[ item->giTag ] = cg.predictedPlayerState.commandTime - ( cg.predictedPlayerState.commandTime % 1000 ); + // this assumption is correct only on transition and implies hardcoded 1.3 coefficient: + if ( item->giTag == PW_HASTE ) { + cg.predictedPlayerState.speed *= 1.3f; + } + } + cg.predictedPlayerState.powerups[ item->giTag ] += cent->currentState.time2 * 1000; + } + + // holdable prediction + if ( item->giType == IT_HOLDABLE && ( item->giTag == HI_TELEPORTER || item->giTag == HI_MEDKIT ) ) { + cg.predictedPlayerState.stats[ STAT_HOLDABLE_ITEM ] = item - bg_itemlist; + } +} + + /* =================== CG_TouchItem @@ -244,20 +471,25 @@ CG_TouchItem static void CG_TouchItem( centity_t *cent ) { const gitem_t *item; + if ( cg.allowPickupPrediction && cg.allowPickupPrediction > cg.time ) { + return; + } + if ( !cg_predictItems.integer ) { return; } + if ( !BG_PlayerTouchesItem( &cg.predictedPlayerState, ¢->currentState, cg.time ) ) { return; } // never pick an item up twice in a prediction - if ( cent->miscTime == cg.time ) { + if ( cent->delaySpawn > cg.time ) { return; } if ( !BG_CanItemBeGrabbed( cgs.gametype, ¢->currentState, &cg.predictedPlayerState ) ) { - return; // can't hold it + return; // can't hold it } item = &bg_itemlist[ cent->currentState.modelindex ]; @@ -283,7 +515,10 @@ static void CG_TouchItem( centity_t *cent ) { } // grab it - BG_AddPredictableEventToPlayerstate( EV_ITEM_PICKUP, cent->currentState.modelindex , &cg.predictedPlayerState); + BG_AddPredictableEventToPlayerstate( EV_ITEM_PICKUP, cent->currentState.modelindex , &cg.predictedPlayerState, cent - cg_entities ); + + // perform prediction + CG_PickupPrediction( cent, item ); // remove it from the frame so it won't be drawn cent->currentState.eFlags |= EF_NODRAW; @@ -291,6 +526,10 @@ static void CG_TouchItem( centity_t *cent ) { // don't touch it again this prediction cent->miscTime = cg.time; + // delay next potential pickup for some time + cent->delaySpawn = cg.time + ( cg.meanPing > 0 ? cg.meanPing * 2 + 100 : 333 ); + cent->delaySpawnPlayed = qfalse; + // if it's a weapon, give them some predicted ammo so the autoswitch will work if ( item->giType == IT_WEAPON ) { cg.predictedPlayerState.stats[ STAT_WEAPONS ] |= 1 << item->giTag; @@ -367,6 +606,259 @@ static void CG_TouchTriggerPrediction( void ) { } +static void CG_CheckTimers( void ) { + int i, t; + + // no prediction for spectators + if ( cg.predictedPlayerState.pm_type == PM_SPECTATOR ) { + return; + } + + t = cg.predictedPlayerState.commandTime; + + // no armor/health/powerups prediction for dead bodies + if ( cg.predictedPlayerState.stats[STAT_HEALTH] <= 0 ) + return; + + // periodic tasks + if ( cg.timeResidual && cg.predictedPlayerState.commandTime >= cg.timeResidual && !cg.thisFrameTeleport ) { + cg.timeResidual += 1000; + if ( cg.predictedPlayerState.powerups[ PW_REGEN ] ) { + int maxhealth = cg.predictedPlayerState.stats[ STAT_MAX_HEALTH ]; + if ( cg.predictedPlayerState.stats[ STAT_HEALTH ] < maxhealth ) { + cg.predictedPlayerState.stats[ STAT_HEALTH ] += 15; + if ( cg.predictedPlayerState.stats[ STAT_HEALTH ] > maxhealth * 1.1 ) { + cg.predictedPlayerState.stats[ STAT_HEALTH ] = maxhealth * 1.1; + } + // TODO: add external EV_POWERUP_REGEN + } else if ( cg.predictedPlayerState.stats[ STAT_HEALTH ] < maxhealth * 2) { + cg.predictedPlayerState.stats[ STAT_HEALTH ] += 5; + if ( cg.predictedPlayerState.stats[ STAT_HEALTH ] > maxhealth * 2 ) { + cg.predictedPlayerState.stats[ STAT_HEALTH ] = maxhealth * 2; + } + // TODO: add external EV_POWERUP_REGEN + } + } else { + if ( cg.predictedPlayerState.stats[ STAT_HEALTH ] > cg.predictedPlayerState.stats[ STAT_MAX_HEALTH ] ) { + cg.predictedPlayerState.stats[ STAT_HEALTH ]--; + } + } + if ( cg.predictedPlayerState.stats[ STAT_ARMOR ] > cg.predictedPlayerState.stats[ STAT_MAX_HEALTH ] ) { + cg.predictedPlayerState.stats[ STAT_ARMOR ]--; + } + } + + // turn off any expired powerups + for ( i = 0 ; i < MAX_POWERUPS ; i++ ) { + if ( !cg.predictedPlayerState.powerups[ i ] ) + continue; + if ( cg.predictedPlayerState.powerups[ i ] < cg.predictedPlayerState.commandTime ) { + cg.predictedPlayerState.powerups[ i ] = 0; + } + } +} + + +static int CG_IsUnacceptableError( playerState_t *ps, playerState_t *pps, qboolean *forceMove ) { + vec3_t delta; + int i, n, v0, v1; + + if ( pps->pm_time != ps->pm_time || + pps->pm_type != ps->pm_type || + pps->pm_flags != ps->pm_flags ) { + return 1; + } + + VectorSubtract( pps->origin, ps->origin, delta ); + if ( VectorLengthSquared( delta ) > 0.01f * 0.01f ) { + if( cg_showmiss.integer > 2 ) { + CG_Printf( "origin delta: %.2f ", VectorLength( delta ) ); + } + return 2; + } + + VectorSubtract( pps->velocity, ps->velocity, delta ); + if( VectorLengthSquared( delta ) > 0.01f * 0.01f ) { + if( cg_showmiss.integer > 2 ) { + CG_Printf( "velocity 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 ) { + if ( cg_showmiss.integer > 1 ) + CG_Printf( "%i %i %i %i => %i %i %i %i", + pps->weaponTime, pps->gravity, pps->speed, pps->groundEntityNum, + ps->weaponTime, ps->gravity, ps->speed, ps->groundEntityNum ); + + return 4; + } + + // forward gesture animation + if ( pps->torsoAnim != ps->torsoAnim && (ps->torsoAnim & ~ANIM_TOGGLEBIT ) == TORSO_GESTURE ) { + for ( n = 0 ; n < NUM_SAVED_STATES; n++ ) { + cg.savedPmoveStates[ n ].torsoAnim = ps->torsoAnim; + cg.savedPmoveStates[ n ].torsoTimer = ps->torsoTimer; + } + } + + 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.01f * 0.01f ) + return 6; + + // check/update eFlags if needed + v0 = pps->eFlags & EF_NOPREDICT; + v1 = ps->eFlags & EF_NOPREDICT; + if ( v0 != v1 ) { + for ( i = 0 ; i < NUM_SAVED_STATES; i++ ) { + cg.savedPmoveStates[ i ].eFlags = (cg.savedPmoveStates[ i ].eFlags & ~EF_NOPREDICT) | v1 ; + } + pps->eFlags = (pps->eFlags & ~EF_NOPREDICT) | v1; + } + + if ( pps->eFlags != ps->eFlags ) { + if ( cg_showmiss.integer > 1 ) + CG_Printf( "eFlags %i => %i", 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 ] ) { + if ( cg_showmiss.integer > 1 ) { + CG_Printf( "event[%i] %i => %i\n", i, pps->events[ i ], ps->events[ i ] ); + } + return 9; + } + if ( pps->eventParms[ i ] != ps->eventParms[ i ] ) { + if ( cg_showmiss.integer > 1 ) { + CG_Printf( "eventParms[%i] %i => %i\n", 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 ( fabs( AngleDelta( ps->viewangles[ 0 ], pps->viewangles[ 0 ] ) ) > 1.0f || + fabs( AngleDelta( ps->viewangles[ 1 ], pps->viewangles[ 1 ] ) ) > 1.0f || + fabs( AngleDelta( ps->viewangles[ 2 ], pps->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 ) { + if ( cg_showmiss.integer > 1 ) + CG_Printf( "dmg %i %i %i %i >= %i %i %i %i\n", + pps->damageEvent, pps->damageYaw, pps->damagePitch, pps->damageCount, + ps->damageEvent, ps->damageYaw, ps->damagePitch, ps->damageCount ); + return 14; + } + + // health countdown? + if ( pps->stats[ STAT_HEALTH ] == ps->stats[ STAT_HEALTH ] + 1 && ps->stats[ STAT_HEALTH ] >= ps->stats[ STAT_MAX_HEALTH ] ) { + cg.timeResidual = ps->commandTime + 1000; + for ( n = 0 ; n < NUM_SAVED_STATES; n++ ) { + cg.savedPmoveStates[ n ].stats[ STAT_HEALTH ] = ps->stats[ STAT_HEALTH ]; + } + + } + // armor countdown? + if ( pps->stats[ STAT_ARMOR ] == ps->stats[ STAT_ARMOR ] - 1 && ps->stats[ STAT_ARMOR ] >= ps->stats[ STAT_MAX_HEALTH ] ) { + // we may need few frames to sync with client->timeResidual on server side + cg.timeResidual = ps->commandTime + 1000; + for ( n = 0 ; n < NUM_SAVED_STATES; n++ ) { + cg.savedPmoveStates[ n ].stats[ STAT_ARMOR ] = ps->stats[ STAT_ARMOR ]; + } + } + + for( i = 0; i < MAX_STATS; i++ ) { + // we can't predict some flags + if ( i == STAT_CLIENTS_READY /*|| i == STAT_MAX_HEALTH */ ) { + for ( n = 0 ; n < NUM_SAVED_STATES; n++ ) { + cg.savedPmoveStates[ n ].stats[ i ] = ps->stats[ i ]; + } + continue; + } + if ( pps->stats[ i ] != ps->stats[ i ] ) { + if ( cg_showmiss.integer > 1 ) { + CG_Printf( "stats[%i] %i => %i ", i, pps->stats[ i ], ps->stats[ i ] ); + } + return 15; + } + } + + + for( i = 0; i < MAX_PERSISTANT ; i++ ) + { + if ( pps->persistant[ i ] != ps->persistant[ i ] ) { + if ( i >= PERS_TEAM && i <= PERS_PLAYEREVENTS ) { + if ( cg_showmiss.integer > 1 ) { + CG_Printf( "persistant[%i] %i => %i ", i, pps->persistant[ i ], ps->persistant[ i ] ); + } + return 16; + } + v0 = ps->persistant[ i ]; + for ( n = 0 ; n < NUM_SAVED_STATES; n++ ) { + cg.savedPmoveStates[ n ].persistant[ i ] = v0; + } + *forceMove = qtrue; + } + } + + for( i = 0; i < MAX_WEAPONS; i++ ) { + if( pps->ammo[ i ] != ps->ammo[ i ] ) { + if ( cg_showmiss.integer > 1 ) { + CG_Printf( "ammo[%i] %i => %i ", i, pps->ammo[ i ], ps->ammo[ i ] ); + } + return 18; + } + } + + if ( pps->generic1 != ps->generic1 || pps->loopSound != ps->loopSound ) { + return 19; + } + + for ( i = 0; i < MAX_POWERUPS; i++ ) { + if( pps->powerups[ i ] != ps->powerups[ i ] ) { + if ( cg_showmiss.integer > 1 ) + CG_Printf( "powerups[%i] %i => %i ", i, pps->powerups[i], ps->powerups[i] ); + return 20; + } + } + + return 0; +} + /* ================= @@ -400,6 +892,7 @@ void CG_PredictPlayerState( void ) { qboolean moved; usercmd_t oldestCmd; usercmd_t latestCmd; + int stateIndex = 0, predictCmd = 0; cg.hyperspace = qfalse; // will be set if touching a trigger_teleport @@ -437,7 +930,6 @@ void CG_PredictPlayerState( void ) { if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_SPECTATOR ) { cg_pmove.tracemask &= ~CONTENTS_BODY; // spectators can fly through bodies } - cg_pmove.noFootsteps = ( cgs.dmflags & DF_NO_FOOTSTEPS ) > 0; // save the state before the pmove so we can detect transitions oldPlayerState = cg.predictedPlayerState; @@ -449,7 +941,7 @@ void CG_PredictPlayerState( void ) { // the last good position we had cmdNum = current - CMD_BACKUP + 1; trap_GetUserCmd( cmdNum, &oldestCmd ); - if ( oldestCmd.serverTime > cg.snap->ps.commandTime + if ( oldestCmd.serverTime > cg.snap->ps.commandTime && oldestCmd.serverTime < cg.time ) { // special check for map_restart if ( cg_showmiss.integer ) { CG_Printf ("exceeded PACKET_BACKUP on commands\n"); @@ -475,13 +967,105 @@ void CG_PredictPlayerState( void ) { cg_pmove.pmove_fixed = cgs.pmove_fixed; cg_pmove.pmove_msec = cgs.pmove_msec; + // clean event stack + eventStack = 0; + // run cmds moved = qfalse; - for ( cmdNum = current - CMD_BACKUP + 1 ; cmdNum <= current ; cmdNum++ ) { + + cg_pmove.pmove_fixed = cgs.pmove_fixed; + cg_pmove.pmove_msec = cgs.pmove_msec; + + // 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. + if( 1 /* cg_optimizePrediction.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; + int errorcode; + 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 ) { + continue; + } + // make sure the state differences are acceptable + errorcode = CG_IsUnacceptableError( &cg.predictedPlayerState, &cg.savedPmoveStates[ i ], &moved ); + if ( errorcode ) { + if( cg_showmiss.integer > 1 ) + CG_Printf( "errorcode %d at %d\n", errorcode, cg.time ); + 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; + } + + cmdNum = current - CMD_BACKUP + 1; + if ( cmdNum < 0 ) // can happen on first spawn + cmdNum = 0; + + // run cmds + // moved = qfalse; + + for ( /* cmdNum = current - CMD_BACKUP + 1 */; cmdNum <= current ; cmdNum++ ) { // get the command trap_GetUserCmd( cmdNum, &cg_pmove.cmd ); - if ( cg_pmove.pmove_fixed ) { + if ( cgs.pmove_fixed ) { PM_UpdateViewAngles( cg_pmove.ps, &cg_pmove.cmd ); } @@ -511,21 +1095,24 @@ void CG_PredictPlayerState( void ) { CG_Printf( "PredictionTeleport\n" ); } cg.thisFrameTeleport = qfalse; + + // delay prediction for some time or until first server event + cg.allowPickupPrediction = cg.time + PICKUP_PREDICTION_DELAY; } else { vec3_t adjusted, new_angles; CG_AdjustPositionForMover( cg.predictedPlayerState.origin, - cg.predictedPlayerState.groundEntityNum, cg.physicsTime, cg.oldTime, adjusted, cg.predictedPlayerState.viewangles, new_angles); + cg.predictedPlayerState.groundEntityNum, cg.physicsTime, cg.oldTime, adjusted, cg.predictedPlayerState.viewangles, new_angles); if ( cg_showmiss.integer ) { - if (!VectorCompare( oldPlayerState.origin, adjusted )) { - CG_Printf("prediction error\n"); + if ( !VectorCompare( oldPlayerState.origin, adjusted ) ) { + CG_Printf( "prediction error\n" ); } } VectorSubtract( oldPlayerState.origin, adjusted, delta ); - len = VectorLength( delta ); - if ( len > 0.1 ) { + len = VectorLengthSquared( delta ); + if ( len > (0.01f * 0.01f) ) { if ( cg_showmiss.integer ) { - CG_Printf("Prediction miss: %f\n", len); + CG_Printf( "Prediction miss: %f\n", sqrt( len ) ); } if ( cg_errorDecay.integer ) { int t; @@ -556,19 +1143,41 @@ void CG_PredictPlayerState( void ) { if ( cg_pmove.pmove_fixed ) { cg_pmove.cmd.serverTime = ((cg_pmove.cmd.serverTime + cg_pmove.pmove_msec-1) / cg_pmove.pmove_msec) * cg_pmove.pmove_msec; } - - Pmove (&cg_pmove); +#if 0 + if ( !cg_optimizePrediction.integer ) { + Pmove (&cg_pmove); + } else +#endif + if ( /*cg_optimizePrediction.integer && */ ( cmdNum >= predictCmd || ( stateIndex + 1 ) % NUM_SAVED_STATES == cg.stateHead ) ) { + + Pmove( &cg_pmove ); + + // add push trigger movement effects + CG_TouchTriggerPrediction(); + + // check for expired powerups etc. + CG_CheckTimers(); + + // 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 { + *cg_pmove.ps = cg.savedPmoveStates[ stateIndex ]; + stateIndex = ( stateIndex + 1 ) % NUM_SAVED_STATES; + } moved = qtrue; - - // add push trigger movement effects - CG_TouchTriggerPrediction(); - - // check for predictable events that changed from previous predictions - //CG_CheckChangedPredictableEvents(&cg.predictedPlayerState); } - if ( cg_showmiss.integer > 1 ) { + if ( cg_showmiss.integer > 3 ) { CG_Printf( "[%i : %i] ", cg_pmove.cmd.serverTime, cg.time ); } @@ -576,17 +1185,19 @@ void CG_PredictPlayerState( void ) { if ( cg_showmiss.integer ) { CG_Printf( "not moved\n" ); } + // clean event stack + eventStack = 0; return; } // adjust for the movement of the groundentity - CG_AdjustPositionForMover( cg.predictedPlayerState.origin, - cg.predictedPlayerState.groundEntityNum, - cg.physicsTime, cg.time, cg.predictedPlayerState.origin, cg.predictedPlayerState.viewangles, cg.predictedPlayerState.viewangles); + CG_AdjustPositionForMover( cg.predictedPlayerState.origin, cg.predictedPlayerState.groundEntityNum, + cg.physicsTime, cg.time, cg.predictedPlayerState.origin, + cg.predictedPlayerState.viewangles, cg.predictedPlayerState.viewangles ); if ( cg_showmiss.integer ) { - if (cg.predictedPlayerState.eventSequence > oldPlayerState.eventSequence + MAX_PS_EVENTS) { - CG_Printf("WARNING: dropped event\n"); + if ( cg.predictedPlayerState.eventSequence > oldPlayerState.eventSequence + MAX_PS_EVENTS ) { + CG_Printf( "WARNING: dropped event\n" ); } } @@ -594,11 +1205,9 @@ void CG_PredictPlayerState( void ) { CG_TransitionPlayerState( &cg.predictedPlayerState, &oldPlayerState ); if ( cg_showmiss.integer ) { - if (cg.eventSequence > cg.predictedPlayerState.eventSequence) { - CG_Printf("WARNING: double event\n"); + if ( cg.eventSequence > cg.predictedPlayerState.eventSequence ) { + CG_Printf( "WARNING: double event\n" ); cg.eventSequence = cg.predictedPlayerState.eventSequence; } } } - - diff --git a/code/cgame/cg_view.c b/code/cgame/cg_view.c index 1022a74c..08e1e03e 100644 --- a/code/cgame/cg_view.c +++ b/code/cgame/cg_view.c @@ -897,7 +897,5 @@ void CG_DrawActiveFrame( int serverTime, stereoFrame_t stereoView, qboolean demo if ( cg_stats.integer ) { CG_Printf( "cg.clientFrame:%i\n", cg.clientFrame ); } - - } diff --git a/code/game/bg_misc.c b/code/game/bg_misc.c index 7cccf3d4..2cdff0fc 100644 --- a/code/game/bg_misc.c +++ b/code/game/bg_misc.c @@ -1372,10 +1372,13 @@ BG_AddPredictableEventToPlayerstate Handles the sequence numbers =============== */ +#ifdef CGAME +void CG_StoreEvent( entity_event_t ev, int eventParm, int entityNum ); +#endif void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize ); -void BG_AddPredictableEventToPlayerstate( int newEvent, int eventParm, playerState_t *ps ) { +void BG_AddPredictableEventToPlayerstate( entity_event_t newEvent, int eventParm, playerState_t *ps, int entityNum ) { #ifdef _DEBUG { @@ -1390,11 +1393,17 @@ void BG_AddPredictableEventToPlayerstate( int newEvent, int eventParm, playerSta } } #endif + +#ifdef CGAME + CG_StoreEvent( newEvent, eventParm, entityNum ); +#endif + ps->events[ps->eventSequence & (MAX_PS_EVENTS-1)] = newEvent; ps->eventParms[ps->eventSequence & (MAX_PS_EVENTS-1)] = eventParm; ps->eventSequence++; } + /* ======================== BG_TouchJumpPad @@ -1426,7 +1435,7 @@ void BG_TouchJumpPad( playerState_t *ps, entityState_t *jumppad ) { } else { effectNum = 1; } - BG_AddPredictableEventToPlayerstate( EV_JUMP_PAD, effectNum, ps ); + BG_AddPredictableEventToPlayerstate( EV_JUMP_PAD, effectNum, ps, -1 ); } // remember hitting this jumppad this frame ps->jumppad_ent = jumppad->number; diff --git a/code/game/bg_pmove.c b/code/game/bg_pmove.c index b9c43db8..f90819b4 100644 --- a/code/game/bg_pmove.c +++ b/code/game/bg_pmove.c @@ -39,7 +39,7 @@ PM_AddEvent =============== */ void PM_AddEvent( int newEvent ) { - BG_AddPredictableEventToPlayerstate( newEvent, 0, pm->ps ); + BG_AddPredictableEventToPlayerstate( newEvent, 0, pm->ps, -1 ); } /* @@ -53,7 +53,8 @@ void PM_AddTouchEnt( int entityNum ) { if ( entityNum == ENTITYNUM_WORLD ) { return; } - if ( pm->numtouch == MAXTOUCH ) { + + if ( pm->numtouch >= MAXTOUCH ) { return; } @@ -1401,7 +1402,7 @@ static void PM_Footsteps( void ) { if ( ( ( old + 64 ) ^ ( pm->ps->bobCycle + 64 ) ) & 128 ) { if ( pm->waterlevel == 0 ) { // on ground will only play sounds if running - if ( footstep && !pm->noFootsteps ) { + if ( footstep ) { PM_AddEvent( PM_FootstepForSurface() ); } } else if ( pm->waterlevel == 1 ) { diff --git a/code/game/bg_public.h b/code/game/bg_public.h index 7e27d2bc..d400f757 100644 --- a/code/game/bg_public.h +++ b/code/game/bg_public.h @@ -11,6 +11,9 @@ #define GIB_HEALTH -40 #define ARMOR_PROTECTION 0.66 +#define HEALTH_SOFT_LIMIT 100 +#define AMMO_HARD_LIMIT 200 + #define MAX_ITEMS 256 #define RANK_TIED_FLAG 0x4000 @@ -151,7 +154,6 @@ typedef struct { usercmd_t cmd; int tracemask; // collide against these types of surfaces int debugLevel; // if set, diagnostic output will be printed - qboolean noFootsteps; // if the game is setup for no footsteps by the server qboolean gauntletHit; // true if a gauntlet attack would actually hit something int framecount; @@ -254,6 +256,8 @@ typedef enum { #define EF_PERSISTANT ( EF_CONNECTION | EF_VOTED | EF_TEAMVOTED ) #define EF_AWARDS ( EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET | EF_AWARD_ASSIST | EF_AWARD_DEFEND | EF_AWARD_CAP ) +#define EF_NOPREDICT ( EF_AWARDS | EF_PERSISTANT | EF_TALK ) + // NOTE: may not have more than 16 typedef enum { PW_NONE, @@ -694,7 +698,7 @@ typedef enum { void BG_EvaluateTrajectory( const trajectory_t *tr, int atTime, vec3_t result ); void BG_EvaluateTrajectoryDelta( const trajectory_t *tr, int atTime, vec3_t result ); -void BG_AddPredictableEventToPlayerstate( int newEvent, int eventParm, playerState_t *ps ); +void BG_AddPredictableEventToPlayerstate( entity_event_t newEvent, int eventParm, playerState_t *ps, int entityNum ); void BG_TouchJumpPad( playerState_t *ps, entityState_t *jumppad ); diff --git a/code/game/g_active.c b/code/game/g_active.c index 25674281..42b04675 100644 --- a/code/game/g_active.c +++ b/code/game/g_active.c @@ -568,6 +568,8 @@ void ClientEvents( gentity_t *ent, int oldEventSequence ) { if ( drop->count < 1 ) { drop->count = 1; } + // for pickup prediction + drop->s.time2 = drop->count; ent->client->ps.powerups[ j ] = 0; } @@ -742,7 +744,7 @@ void ClientThink_real( gentity_t *ent ) { if ( ucmd->serverTime > level.time + 200 ) { ucmd->serverTime = level.time + 200; // G_Printf("serverTime <<<<<\n" ); - } + } else if ( ucmd->serverTime < level.time - 1000 ) { ucmd->serverTime = level.time - 1000; // G_Printf("serverTime >>>>>\n" ); @@ -800,7 +802,7 @@ void ClientThink_real( gentity_t *ent ) { // clear the rewards if time if ( level.time > client->rewardTime ) { - client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET | EF_AWARD_ASSIST | EF_AWARD_DEFEND | EF_AWARD_CAP ); + client->ps.eFlags &= ~EF_AWARDS; } if ( client->noclip ) { @@ -890,7 +892,6 @@ void ClientThink_real( gentity_t *ent ) { pm.trace = trap_Trace; pm.pointcontents = trap_PointContents; pm.debugLevel = g_debugMove.integer; - pm.noFootsteps = ( g_dmflags.integer & DF_NO_FOOTSTEPS ) > 0; pm.pmove_fixed = pmove_fixed.integer; pm.pmove_msec = pmove_msec.integer; @@ -1099,7 +1100,7 @@ void ClientEndFrame( gentity_t *ent ) { // turn off any expired powerups for ( i = 0 ; i < MAX_POWERUPS ; i++ ) { - if ( client->ps.powerups[ i ] < level.time ) { + if ( client->ps.powerups[ i ] < client->pers.cmd.serverTime ) { client->ps.powerups[ i ] = 0; } } diff --git a/code/game/g_client.c b/code/game/g_client.c index 1b1e407c..74e55512 100644 --- a/code/game/g_client.c +++ b/code/game/g_client.c @@ -644,19 +644,19 @@ qboolean ClientUserinfoChanged( int clientNum ) { // set max health #ifdef MISSIONPACK if (client->ps.powerups[PW_GUARD]) { - client->pers.maxHealth = 200; + client->pers.maxHealth = HEALTH_SOFT_LIMIT*2; } else { health = atoi( Info_ValueForKey( userinfo, "handicap" ) ); client->pers.maxHealth = health; - if ( client->pers.maxHealth < 1 || client->pers.maxHealth > 100 ) { - client->pers.maxHealth = 100; + if ( client->pers.maxHealth < 1 || client->pers.maxHealth > HEALTH_SOFT_LIMIT ) { + client->pers.maxHealth = HEALTH_SOFT_LIMIT; } } #else health = atoi( Info_ValueForKey( userinfo, "handicap" ) ); client->pers.maxHealth = health; - if ( client->pers.maxHealth < 1 || client->pers.maxHealth > 100 ) { - client->pers.maxHealth = 100; + if ( client->pers.maxHealth < 1 || client->pers.maxHealth > HEALTH_SOFT_LIMIT ) { + client->pers.maxHealth = HEALTH_SOFT_LIMIT; } #endif client->ps.stats[STAT_MAX_HEALTH] = client->pers.maxHealth; @@ -681,15 +681,6 @@ qboolean ClientUserinfoChanged( int clientNum ) { client->pers.teamInfo = qfalse; } #endif - /* - s = Info_ValueForKey( userinfo, "cg_pmove_fixed" ); - if ( !*s || atoi( s ) == 0 ) { - client->pers.pmoveFixed = qfalse; - } - else { - client->pers.pmoveFixed = qtrue; - } - */ // set model Q_strncpyz( model, Info_ValueForKey( userinfo, "model" ), sizeof( model ) ); @@ -1062,8 +1053,8 @@ void ClientSpawn(gentity_t *ent) { trap_GetUserinfo( index, userinfo, sizeof(userinfo) ); // set max health client->pers.maxHealth = atoi( Info_ValueForKey( userinfo, "handicap" ) ); - if ( client->pers.maxHealth < 1 || client->pers.maxHealth > 100 ) { - client->pers.maxHealth = 100; + if ( client->pers.maxHealth < 1 || client->pers.maxHealth > HEALTH_SOFT_LIMIT ) { + client->pers.maxHealth = HEALTH_SOFT_LIMIT; } // clear entity values client->ps.stats[STAT_MAX_HEALTH] = client->pers.maxHealth; diff --git a/code/game/g_combat.c b/code/game/g_combat.c index 48a29da6..1e26b3c0 100644 --- a/code/game/g_combat.c +++ b/code/game/g_combat.c @@ -82,7 +82,10 @@ void TossClientItems( gentity_t *self ) { item = BG_FindItemForWeapon( weapon ); // spawn the item - Drop_Item( self, item, 0 ); + drop = Drop_Item( self, item, 0 ); + + // for pickup prediction + drop->s.time2 = item->quantity; } // drop all the powerups if not in teamplay @@ -100,14 +103,16 @@ void TossClientItems( gentity_t *self ) { if ( drop->count < 1 ) { drop->count = 1; } + // for pickup prediction + drop->s.time2 = drop->count; angle += 45; } } } } -#ifdef MISSIONPACK +#ifdef MISSIONPACK /* ================= TossClientCubes @@ -533,11 +538,13 @@ void player_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int // if I committed suicide, the flag does not fall, it returns. if (meansOfDeath == MOD_SUICIDE) { +#ifdef MISSIONPACK if ( self->client->ps.powerups[PW_NEUTRALFLAG] ) { // only happens in One Flag CTF Team_ReturnFlag( TEAM_FREE ); self->client->ps.powerups[PW_NEUTRALFLAG] = 0; - } - else if ( self->client->ps.powerups[PW_REDFLAG] ) { // only happens in standard CTF + } else +#endif + if ( self->client->ps.powerups[PW_REDFLAG] ) { // only happens in standard CTF Team_ReturnFlag( TEAM_RED ); self->client->ps.powerups[PW_REDFLAG] = 0; } diff --git a/code/game/g_items.c b/code/game/g_items.c index ad0b6f1f..b9935af6 100644 --- a/code/game/g_items.c +++ b/code/game/g_items.c @@ -188,14 +188,16 @@ int Pickup_Holdable( gentity_t *ent, gentity_t *other ) { //====================================================================== + void Add_Ammo (gentity_t *ent, int weapon, int count) { ent->client->ps.ammo[weapon] += count; - if ( ent->client->ps.ammo[weapon] > 200 ) { - ent->client->ps.ammo[weapon] = 200; + if ( ent->client->ps.ammo[weapon] > AMMO_HARD_LIMIT ) { + ent->client->ps.ammo[weapon] = AMMO_HARD_LIMIT; } } + int Pickup_Ammo (gentity_t *ent, gentity_t *other) { int quantity; @@ -441,7 +443,11 @@ void Touch_Item (gentity_t *ent, gentity_t *other, trace_t *trace) { break; case IT_POWERUP: respawn = Pickup_Powerup(ent, other); - predict = qfalse; + // allow prediction for some powerups + if ( ent->item->giTag >= PW_QUAD && ent->item->giTag <= PW_FLIGHT ) + predict = qtrue; + else + predict = qfalse; break; #ifdef MISSIONPACK case IT_PERSISTANT_POWERUP: @@ -656,6 +662,13 @@ void FinishSpawningItem( gentity_t *ent ) { // using an item causes it to respawn ent->use = Use_Item; + // for pickup prediction + if ( ent->count ) { + ent->s.time2 = ent->count; + } else if ( ent->item ) { + ent->s.time2 = ent->item->quantity; + } + if ( ent->spawnflags & 1 ) { // suspended G_SetOrigin( ent, ent->s.origin ); @@ -694,8 +707,7 @@ void FinishSpawningItem( gentity_t *ent ) { return; } - - trap_LinkEntity (ent); + trap_LinkEntity( ent ); } diff --git a/code/game/g_utils.c b/code/game/g_utils.c index a61a1f1b..3b47e034 100644 --- a/code/game/g_utils.c +++ b/code/game/g_utils.c @@ -546,7 +546,7 @@ void G_AddPredictableEvent( gentity_t *ent, entity_event_t event, int eventParm if ( !ent->client ) { return; } - BG_AddPredictableEventToPlayerstate( event, eventParm, &ent->client->ps ); + BG_AddPredictableEventToPlayerstate( event, eventParm, &ent->client->ps, -1 ); }