From 0720d5de5ba337943647ad4597b0ae2af450839d Mon Sep 17 00:00:00 2001 From: milaq Date: Sat, 12 Oct 2019 01:58:46 +0200 Subject: [PATCH 01/26] Add additional dmflags from OpenArena * 64: Instant weapon change * 128: Non-accelerated jumping * 256: Total invisibility * 512: Light voting * 1024: No self damage from weapons --- code/cgame/cg_players.c | 6 ++- code/cgame/cg_weapons.c | 6 ++- code/game/bg_pmove.c | 84 ++++++++++++++++++++++------------------- code/game/bg_public.h | 12 ++++-- code/game/g_active.c | 1 + code/game/g_combat.c | 5 +++ code/game/g_main.c | 18 ++++++++- 7 files changed, 85 insertions(+), 47 deletions(-) diff --git a/code/cgame/cg_players.c b/code/cgame/cg_players.c index 4a4abaadd8..82cfafdcc7 100644 --- a/code/cgame/cg_players.c +++ b/code/cgame/cg_players.c @@ -2150,8 +2150,10 @@ Also called by CG_Missile for quad rockets, but nobody can tell... void CG_AddRefEntityWithPowerups( refEntity_t *ent, entityState_t *state, int team ) { if ( state->powerups & ( 1 << PW_INVIS ) ) { - ent->customShader = cgs.media.invisShader; - trap_R_AddRefEntityToScene( ent ); + if ( (cgs.dmflags & DF_TOTAL_INVIS) == 0) { + ent->customShader = cgs.media.invisShader; + trap_R_AddRefEntityToScene( ent ); + } } else { /* if ( state->eFlags & EF_KAMIKAZE ) { diff --git a/code/cgame/cg_weapons.c b/code/cgame/cg_weapons.c index 644292eeea..24871632e8 100644 --- a/code/cgame/cg_weapons.c +++ b/code/cgame/cg_weapons.c @@ -1169,8 +1169,10 @@ CG_AddWeaponWithPowerups static void CG_AddWeaponWithPowerups( refEntity_t *gun, int powerups ) { // add powerup effects if ( powerups & ( 1 << PW_INVIS ) ) { - gun->customShader = cgs.media.invisShader; - trap_R_AddRefEntityToScene( gun ); + if ( (cgs.dmflags & DF_TOTAL_INVIS) == 0) { + gun->customShader = cgs.media.invisShader; + trap_R_AddRefEntityToScene( gun ); + } } else { trap_R_AddRefEntityToScene( gun ); diff --git a/code/game/bg_pmove.c b/code/game/bg_pmove.c index 5465c57927..b67192d7c2 100644 --- a/code/game/bg_pmove.c +++ b/code/game/bg_pmove.c @@ -237,42 +237,42 @@ Handles user intended acceleration ============== */ static void PM_Accelerate( vec3_t wishdir, float wishspeed, float accel ) { -#if 1 - // q2 style - int i; - float addspeed, accelspeed, currentspeed; - - currentspeed = DotProduct (pm->ps->velocity, wishdir); - addspeed = wishspeed - currentspeed; - if (addspeed <= 0) { - return; - } - accelspeed = accel*pml.frametime*wishspeed; - if (accelspeed > addspeed) { - accelspeed = addspeed; - } + if (! (pm->pmove_flags & DF_NO_BUNNY) ) { + // q2 style + int i; + float addspeed, accelspeed, currentspeed; + + currentspeed = DotProduct (pm->ps->velocity, wishdir); + addspeed = wishspeed - currentspeed; + if (addspeed <= 0) { + return; + } + accelspeed = accel*pml.frametime*wishspeed; + if (accelspeed > addspeed) { + accelspeed = addspeed; + } - for (i=0 ; i<3 ; i++) { - pm->ps->velocity[i] += accelspeed*wishdir[i]; - } -#else - // proper way (avoids strafe jump maxspeed bug), but feels bad - vec3_t wishVelocity; - vec3_t pushDir; - float pushLen; - float canPush; - - VectorScale( wishdir, wishspeed, wishVelocity ); - VectorSubtract( wishVelocity, pm->ps->velocity, pushDir ); - pushLen = VectorNormalize( pushDir ); + for (i=0 ; i<3 ; i++) { + pm->ps->velocity[i] += accelspeed*wishdir[i]; + } + } else { + // proper way (avoids strafe jump maxspeed bug), but feels bad + vec3_t wishVelocity; + vec3_t pushDir; + float pushLen; + float canPush; + + VectorScale( wishdir, wishspeed, wishVelocity ); + VectorSubtract( wishVelocity, pm->ps->velocity, pushDir ); + pushLen = VectorNormalize( pushDir ); + + canPush = accel*pml.frametime*wishspeed; + if (canPush > pushLen) { + canPush = pushLen; + } - canPush = accel*pml.frametime*wishspeed; - if (canPush > pushLen) { - canPush = pushLen; + VectorMA( pm->ps->velocity, canPush, pushDir, pm->ps->velocity ); } - - VectorMA( pm->ps->velocity, canPush, pushDir, pm->ps->velocity ); -#endif } @@ -1478,10 +1478,14 @@ static void PM_BeginWeaponChange( int weapon ) { return; } - PM_AddEvent( EV_CHANGE_WEAPON ); - pm->ps->weaponstate = WEAPON_DROPPING; - pm->ps->weaponTime += 200; - PM_StartTorsoAnim( TORSO_DROP ); + if ( pm->pmove_flags & DF_INSTANT_WEAPON_CHANGE ) { + pm->ps->weaponstate = WEAPON_DROPPING; + } else { + PM_AddEvent( EV_CHANGE_WEAPON ); + pm->ps->weaponstate = WEAPON_DROPPING; + pm->ps->weaponTime += 200; + PM_StartTorsoAnim( TORSO_DROP ); + } } @@ -1504,8 +1508,10 @@ static void PM_FinishWeaponChange( void ) { pm->ps->weapon = weapon; pm->ps->weaponstate = WEAPON_RAISING; - pm->ps->weaponTime += 250; - PM_StartTorsoAnim( TORSO_RAISE ); + if (! (pm->pmove_flags & DF_INSTANT_WEAPON_CHANGE)) { + pm->ps->weaponTime += 250; + PM_StartTorsoAnim( TORSO_RAISE ); + } } diff --git a/code/game/bg_public.h b/code/game/bg_public.h index fa898c4438..2212345ebd 100644 --- a/code/game/bg_public.h +++ b/code/game/bg_public.h @@ -184,6 +184,7 @@ typedef struct { // for fixed msec Pmove int pmove_fixed; int pmove_msec; + int pmove_flags; // callbacks to test the world // these will be different functions during game and cgame @@ -654,9 +655,14 @@ qboolean BG_CanItemBeGrabbed( int gametype, const entityState_t *ent, const play // g_dmflags->integer flags -#define DF_NO_FALLING 8 -#define DF_FIXED_FOV 16 -#define DF_NO_FOOTSTEPS 32 +#define DF_NO_FALLING 8 +#define DF_FIXED_FOV 16 +#define DF_NO_FOOTSTEPS 32 +#define DF_INSTANT_WEAPON_CHANGE 64 +#define DF_NO_BUNNY 128 +#define DF_TOTAL_INVIS 256 +#define DF_LIGHT_VOTING 512 +#define DF_NO_SELF_DAMAGE 1024 // content masks #define MASK_ALL (-1) diff --git a/code/game/g_active.c b/code/game/g_active.c index a506a1e09c..0c84f52002 100644 --- a/code/game/g_active.c +++ b/code/game/g_active.c @@ -920,6 +920,7 @@ void ClientThink_real( gentity_t *ent ) { pm.pmove_fixed = pmove_fixed.integer | client->pers.pmoveFixed; pm.pmove_msec = pmove_msec.integer; + pm.pmove_flags = g_dmflags.integer; VectorCopy( client->ps.origin, client->oldOrigin ); diff --git a/code/game/g_combat.c b/code/game/g_combat.c index dd52bf565e..509cfd7b2e 100644 --- a/code/game/g_combat.c +++ b/code/game/g_combat.c @@ -976,6 +976,11 @@ void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, if ( damage < 1 ) { damage = 1; } + + if ( targ == attacker && (g_dmflags.integer & DF_NO_SELF_DAMAGE) ) { + damage = 0; + } + take = damage; // save some from armor diff --git a/code/game/g_main.c b/code/game/g_main.c index 4398113e4b..d1a280269d 100644 --- a/code/game/g_main.c +++ b/code/game/g_main.c @@ -1587,7 +1587,23 @@ void CheckVote( void ) { return; } if ( level.time - level.voteTime >= VOTE_TIME ) { - trap_SendServerCommand( -1, "print \"Vote failed.\n\"" ); + if ( g_dmflags.integer & DF_LIGHT_VOTING ) { + if ( level.voteYes > level.voteNo*2 ) { + // Let pass if there was at least twice as many for as against + trap_SendServerCommand( -1, "print \"Vote passed. At least 2 of 3 voted yes\n\"" ); + level.voteExecuteTime = level.time + 3000; + } else { + // Let pass if there is more yes than no and at least 2 yes votes and at least 30% yes of all on the server + if ( level.voteYes > level.voteNo && level.voteYes >= 2 && (level.voteYes*10) > (level.numVotingClients*3) ) { + trap_SendServerCommand( -1, "print \"Vote passed. More yes than no.\n\"" ); + level.voteExecuteTime = level.time + 3000; + } else { + trap_SendServerCommand( -1, "print \"Vote failed.\n\"" ); + } + } + } else { + trap_SendServerCommand( -1, "print \"Vote failed.\n\"" ); + } } else { // ATVI Q3 1.32 Patch #9, WNF if ( level.voteYes > level.numVotingClients/2 ) { From deb9768a0866d2d556a62eff2f1e7ad6d37f211a Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Mon, 29 May 2017 10:42:09 -0500 Subject: [PATCH 02/26] Aspect correct HUD and new cvars --- code/cgame/cg_draw.c | 168 +++++++++++++++++++++++++++++++++---- code/cgame/cg_drawtools.c | 86 ++++++++++++++++++- code/cgame/cg_info.c | 2 +- code/cgame/cg_local.h | 30 +++++++ code/cgame/cg_main.c | 40 ++++++++- code/cgame/cg_scoreboard.c | 6 ++ code/cgame/cg_view.c | 14 +++- code/cgame/cg_weapons.c | 54 ++++++++---- 8 files changed, 361 insertions(+), 39 deletions(-) diff --git a/code/cgame/cg_draw.c b/code/cgame/cg_draw.c index afe0941201..46ab4c4211 100644 --- a/code/cgame/cg_draw.c +++ b/code/cgame/cg_draw.c @@ -242,6 +242,10 @@ static void CG_DrawField (int x, int y, int width, int value) { l = width; x += 2 + CHAR_WIDTH*(width - l); + // center x, move y to bottom. + x += (1.0f-cg_statusScale.value)*l*CHAR_WIDTH*0.5f; + y += (1.0f-cg_statusScale.value)*CHAR_HEIGHT; + ptr = num; while (*ptr && l) { @@ -250,8 +254,8 @@ static void CG_DrawField (int x, int y, int width, int value) { else frame = *ptr -'0'; - CG_DrawPic( x,y, CHAR_WIDTH, CHAR_HEIGHT, cgs.media.numberShaders[frame] ); - x += CHAR_WIDTH; + CG_DrawPic( x,y, CHAR_WIDTH*cg_statusScale.value, CHAR_HEIGHT*cg_statusScale.value, cgs.media.numberShaders[frame] ); + x += CHAR_WIDTH*cg_statusScale.value; ptr++; l--; } @@ -302,6 +306,58 @@ void CG_Draw3DModel( float x, float y, float w, float h, qhandle_t model, qhandl trap_R_RenderScene( &refdef ); } +/* +================ +CG_DrawHealthModel + +================ +*/ +void CG_DrawHealthModel( float x, float y, float w, float h, qhandle_t model, qhandle_t skin, qhandle_t model2, vec3_t origin, vec3_t angles, float yaw2 ) { + refdef_t refdef; + refEntity_t ent; + + if ( !cg_draw3dIcons.integer || !cg_drawIcons.integer ) { + return; + } + + CG_AdjustFrom640( &x, &y, &w, &h ); + + memset( &refdef, 0, sizeof( refdef ) ); + + memset( &ent, 0, sizeof( ent ) ); + AnglesToAxis( angles, ent.axis ); + VectorCopy( origin, ent.origin ); + ent.hModel = model; + ent.customSkin = skin; + ent.renderfx = RF_NOSHADOW; // no stencil shadows + + refdef.rdflags = RDF_NOWORLDMODEL; + + AxisClear( refdef.viewaxis ); + + refdef.fov_x = 30; + refdef.fov_y = 30; + + refdef.x = x; + refdef.y = y; + refdef.width = w; + refdef.height = h; + + refdef.time = cg.time; + + trap_R_ClearScene(); + trap_R_AddRefEntityToScene( &ent ); + + if ( model2 ) { + ent.hModel = model2; + angles[YAW] = yaw2; + AnglesToAxis( angles, ent.axis ); + trap_R_AddRefEntityToScene( &ent ); + } + + trap_R_RenderScene( &refdef ); +} + /* ================ CG_DrawHead @@ -465,7 +521,7 @@ static void CG_DrawStatusBarHead( float x ) { angles[YAW] = cg.headStartYaw + ( cg.headEndYaw - cg.headStartYaw ) * frac; angles[PITCH] = cg.headStartPitch + ( cg.headEndPitch - cg.headStartPitch ) * frac; - CG_DrawHead( x, 480 - size, size, size, + CG_DrawHead( x+(1.0f-cg_statusScale.value)*size*0.5f, 480 - size*cg_statusScale.value, size*cg_statusScale.value, size*cg_statusScale.value, cg.snap->ps.clientNum, angles ); } #endif // MISSIONPACK @@ -478,7 +534,8 @@ CG_DrawStatusBarFlag */ #ifndef MISSIONPACK static void CG_DrawStatusBarFlag( float x, int team ) { - CG_DrawFlagModel( x, 480 - ICON_SIZE, ICON_SIZE, ICON_SIZE, team, qfalse ); + int iconSize = ICON_SIZE*cg_statusScale.value; + CG_DrawFlagModel( x+(1.0f-cg_statusScale.value)*ICON_SIZE*0.5f, 480 - iconSize, iconSize, iconSize, team, qfalse ); } #endif // MISSIONPACK @@ -505,7 +562,9 @@ void CG_DrawTeamBackground( int x, int y, int w, int h, float alpha, int team ) return; } trap_R_SetColor( hcolor ); + CG_SetScreenPlacement(PLACE_STRETCH, CG_GetScreenVerticalPlacement()); CG_DrawPic( x, y, w, h, cgs.media.teamStatusBar ); + CG_PopScreenPlacement(); trap_R_SetColor( NULL ); } @@ -524,6 +583,7 @@ static void CG_DrawStatusBar( void ) { vec4_t hcolor; vec3_t angles; vec3_t origin; + float scale, iconSize; static float colors[4][4] = { // { 0.2, 1.0, 0.2, 1.0 } , { 1.0, 0.2, 0.2, 1.0 }, {0.5, 0.5, 0.5, 1} }; @@ -536,8 +596,14 @@ static void CG_DrawStatusBar( void ) { return; } + CG_SetScreenPlacement(PLACE_CENTER, PLACE_BOTTOM); + + scale = Com_Clamp( 0.1f, 2, cg_statusScale.value); + + iconSize = scale * ICON_SIZE; + // draw the team background - CG_DrawTeamBackground( 0, 420, 640, 60, 0.33f, cg.snap->ps.persistant[PERS_TEAM] ); + CG_DrawTeamBackground( 0, 480 - 60*scale, 640, 60*scale, 0.33f, cg.snap->ps.persistant[PERS_TEAM] ); cent = &cg_entities[cg.snap->ps.clientNum]; ps = &cg.snap->ps; @@ -550,11 +616,25 @@ static void CG_DrawStatusBar( void ) { origin[1] = 0; origin[2] = 0; angles[YAW] = 90 + 20 * sin( cg.time / 1000.0 ); - CG_Draw3DModel( CHAR_WIDTH*3 + TEXT_ICON_SPACE, 432, ICON_SIZE, ICON_SIZE, + CG_Draw3DModel( CHAR_WIDTH*3 + TEXT_ICON_SPACE, 480-iconSize, iconSize, iconSize, cg_weapons[ cent->currentState.weapon ].ammoModel, 0, origin, angles ); } - CG_DrawStatusBarHead( 185 + CHAR_WIDTH*3 + TEXT_ICON_SPACE ); + if ( cg_drawStatusHead.integer == 2 ) { + origin[0] = 60; + origin[1] = 0; + origin[2] = -5; + angles[YAW] = ( cg.time & 2047 ) * 360 / 4096.0; + CG_DrawHealthModel( 185 + CHAR_WIDTH*3 + TEXT_ICON_SPACE, 480-iconSize, iconSize, iconSize, + cg_items[ 6 /*item_health_large*/].models[0], 0, cg_items[ 6 /*item_health_large*/].models[1], origin, angles, 0 ); + + // if we didn't draw a 3D icon, draw a 2D icon for health + if ( !cg_draw3dIcons.integer && cg_drawIcons.integer ) { + CG_DrawPic( 185 + CHAR_WIDTH*3 + TEXT_ICON_SPACE, 480 - iconSize, iconSize, iconSize, cg_items[6/*item_health_large*/].icon ); + } + } + else if ( cg_drawStatusHead.integer == 1 ) + CG_DrawStatusBarHead( 185 + CHAR_WIDTH*3 + TEXT_ICON_SPACE ); if( cg.predictedPlayerState.powerups[PW_REDFLAG] ) { CG_DrawStatusBarFlag( 185 + CHAR_WIDTH*3 + TEXT_ICON_SPACE + ICON_SIZE, TEAM_RED ); @@ -569,7 +649,7 @@ static void CG_DrawStatusBar( void ) { origin[1] = 0; origin[2] = -10; angles[YAW] = ( cg.time & 2047 ) * 360 / 2048.0; - CG_Draw3DModel( 370 + CHAR_WIDTH*3 + TEXT_ICON_SPACE, 432, ICON_SIZE, ICON_SIZE, + CG_Draw3DModel( 370 + CHAR_WIDTH*3 + TEXT_ICON_SPACE, 480 - iconSize, iconSize, iconSize, cgs.media.armorModel, 0, origin, angles ); } // @@ -600,7 +680,7 @@ static void CG_DrawStatusBar( void ) { icon = cg_weapons[ cg.predictedPlayerState.weapon ].ammoIcon; if ( icon ) { - CG_DrawPic( CHAR_WIDTH*3 + TEXT_ICON_SPACE, 432, ICON_SIZE, ICON_SIZE, icon ); + CG_DrawPic( CHAR_WIDTH*3 + TEXT_ICON_SPACE, 480 - iconSize, iconSize, iconSize, icon ); } } } @@ -637,7 +717,7 @@ static void CG_DrawStatusBar( void ) { trap_R_SetColor( NULL ); // if we didn't draw a 3D icon, draw a 2D icon for armor if ( !cg_draw3dIcons.integer && cg_drawIcons.integer ) { - CG_DrawPic( 370 + CHAR_WIDTH*3 + TEXT_ICON_SPACE, 432, ICON_SIZE, ICON_SIZE, cgs.media.armorIcon ); + CG_DrawPic( 370 + CHAR_WIDTH*3 + TEXT_ICON_SPACE, 480 - iconSize, iconSize, iconSize, cgs.media.armorIcon ); } } @@ -981,6 +1061,8 @@ static void CG_DrawUpperRight(stereoFrame_t stereoFrame) y = 0; + CG_SetScreenPlacement(PLACE_RIGHT, PLACE_TOP); + if ( cgs.gametype >= GT_TEAM && cg_drawTeamOverlay.integer == 1 ) { y = CG_DrawTeamOverlay( y, qtrue, qtrue ); } @@ -1024,6 +1106,10 @@ static float CG_DrawScores( float y ) { float y1; gitem_t *item; + if ( !cg_drawScores.integer ) { + return y; + } + s1 = cgs.scores1; s2 = cgs.scores2; @@ -1280,6 +1366,8 @@ static void CG_DrawLowerRight( void ) { y = 480 - ICON_SIZE; + CG_SetScreenPlacement(PLACE_RIGHT, PLACE_BOTTOM); + if ( cgs.gametype >= GT_TEAM && cg_drawTeamOverlay.integer == 2 ) { y = CG_DrawTeamOverlay( y, qtrue, qfalse ); } @@ -1298,12 +1386,21 @@ CG_DrawPickupItem static int CG_DrawPickupItem( int y ) { int value; float *fadeColor; + float iconSize, charWidth, charHeight; + + if ( cg_drawPickups.value <= 0 ) { + return y; + } if ( cg.snap->ps.stats[STAT_HEALTH] <= 0 ) { return y; } - y -= ICON_SIZE; + iconSize = ICON_SIZE*cg_drawPickups.value; + charWidth = BIGCHAR_WIDTH*cg_drawPickups.value; + charHeight = BIGCHAR_HEIGHT*cg_drawPickups.value; + + y -= iconSize; value = cg.itemPickup; if ( value ) { @@ -1311,8 +1408,8 @@ static int CG_DrawPickupItem( int y ) { if ( fadeColor ) { CG_RegisterItemVisuals( value ); trap_R_SetColor( fadeColor ); - CG_DrawPic( 8, y, ICON_SIZE, ICON_SIZE, cg_items[ value ].icon ); - CG_DrawBigString( ICON_SIZE + 16, y + (ICON_SIZE/2 - BIGCHAR_HEIGHT/2), bg_itemlist[ value ].pickup_name, fadeColor[0] ); + CG_DrawPic( 8, y, iconSize, iconSize, cg_items[ value ].icon ); + CG_DrawStringExt( iconSize + 16, y + (iconSize/2 - charHeight/2), bg_itemlist[ value ].pickup_name, fadeColor, qfalse, qtrue, charWidth, charHeight, 0 ); trap_R_SetColor( NULL ); } } @@ -1333,6 +1430,8 @@ static void CG_DrawLowerLeft( void ) { y = 480 - ICON_SIZE; + CG_SetScreenPlacement(PLACE_LEFT, PLACE_BOTTOM); + if ( cgs.gametype >= GT_TEAM && cg_drawTeamOverlay.integer == 3 ) { y = CG_DrawTeamOverlay( y, qfalse, qfalse ); } @@ -1367,6 +1466,8 @@ static void CG_DrawTeamInfo( void ) { if (chatHeight <= 0) return; // disabled + CG_SetScreenPlacement( PLACE_LEFT, PLACE_BOTTOM ); + if (cgs.teamLastChatPos != cgs.teamChatPos) { if (cg.time - cgs.teamChatMsgTimes[cgs.teamLastChatPos % chatHeight] > cg_teamChatTime.integer) { cgs.teamLastChatPos++; @@ -1417,6 +1518,8 @@ CG_DrawHoldableItem static void CG_DrawHoldableItem( void ) { int value; + CG_SetScreenPlacement(PLACE_RIGHT, PLACE_CENTER); + value = cg.snap->ps.stats[STAT_HOLDABLE_ITEM]; if ( value ) { CG_RegisterItemVisuals( value ); @@ -1436,6 +1539,8 @@ CG_DrawPersistantPowerup static void CG_DrawPersistantPowerup( void ) { int value; + CG_SetScreenPlacement(PLACE_RIGHT, PLACE_CENTER); + value = cg.snap->ps.stats[STAT_PERSISTANT_POWERUP]; if ( value ) { CG_RegisterItemVisuals( value ); @@ -1461,6 +1566,8 @@ static void CG_DrawReward( void ) { return; } + CG_SetScreenPlacement(PLACE_CENTER, PLACE_CENTER); + color = CG_FadeColor( cg.rewardTime, REWARD_TIME ); if ( !color ) { if (cg.rewardStack > 0) { @@ -1601,6 +1708,8 @@ static void CG_DrawDisconnect( void ) { return; } + CG_SetScreenPlacement(PLACE_CENTER, PLACE_CENTER); + // also add text in center of screen s = "Connection Interrupted"; w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH; @@ -1611,6 +1720,8 @@ static void CG_DrawDisconnect( void ) { return; } + CG_SetScreenPlacement(PLACE_RIGHT, PLACE_BOTTOM); + #ifdef MISSIONPACK x = 640 - 48; y = 480 - 144; @@ -1643,6 +1754,8 @@ static void CG_DrawLagometer( void ) { return; } + CG_SetScreenPlacement(PLACE_RIGHT, PLACE_BOTTOM); + // // draw the graph // @@ -1800,6 +1913,8 @@ static void CG_DrawCenterString( void ) { return; } + CG_SetScreenPlacement(PLACE_CENTER, PLACE_CENTER); + trap_R_SetColor( color ); start = cg.centerPrint; @@ -1881,6 +1996,8 @@ static void CG_DrawCrosshair(void) return; } + CG_SetScreenPlacement(PLACE_CENTER, PLACE_CENTER); + // set color based on health if ( cg_crosshairHealth.integer ) { vec4_t hcolor; @@ -1903,7 +2020,6 @@ static void CG_DrawCrosshair(void) x = cg_crosshairX.integer; y = cg_crosshairY.integer; - CG_AdjustFrom640( &x, &y, &w, &h ); ca = cg_drawCrosshair.integer; if (ca < 0) { @@ -1911,9 +2027,7 @@ static void CG_DrawCrosshair(void) } hShader = cgs.media.crosshairShader[ ca % NUM_CROSSHAIRS ]; - trap_R_DrawStretchPic( x + cg.refdef.x + 0.5 * (cg.refdef.width - w), - y + cg.refdef.y + 0.5 * (cg.refdef.height - h), - w, h, 0, 0, 1, 1, hShader ); + CG_DrawPic( ((SCREEN_WIDTH-w)*0.5f)+x, ((SCREEN_HEIGHT-h)*0.5f)+y, w, h, hShader ); trap_R_SetColor( NULL ); } @@ -2051,6 +2165,8 @@ static void CG_DrawCrosshairNames( void ) { return; } + CG_SetScreenPlacement(PLACE_CENTER, PLACE_CENTER); + // scan the known entities to see if the crosshair is sighted on one CG_ScanForCrosshairEntity(); @@ -2082,6 +2198,7 @@ CG_DrawSpectator ================= */ static void CG_DrawSpectator(void) { + CG_SetScreenPlacement(PLACE_CENTER, PLACE_BOTTOM); CG_DrawBigString(320 - 9 * 8, 440, "SPECTATOR", 1.0F); if ( cgs.gametype == GT_TOURNAMENT ) { CG_DrawBigString(320 - 15 * 8, 460, "waiting to play", 1.0F); @@ -2104,6 +2221,8 @@ static void CG_DrawVote(void) { return; } + CG_SetScreenPlacement(PLACE_LEFT, PLACE_TOP); + // play a talk beep whenever it is modified if ( cgs.voteModified ) { cgs.voteModified = qfalse; @@ -2145,6 +2264,8 @@ static void CG_DrawTeamVote(void) { return; } + CG_SetScreenPlacement(PLACE_LEFT, PLACE_TOP); + // play a talk beep whenever it is modified if ( cgs.teamVoteModified[cs_offset] ) { cgs.teamVoteModified[cs_offset] = qfalse; @@ -2165,6 +2286,8 @@ static qboolean CG_DrawScoreboard( void ) { #ifdef MISSIONPACK static qboolean firstTime = qtrue; + CG_SetScreenPlacement(PLACE_CENTER, PLACE_CENTER); + if (menuScoreboard) { menuScoreboard->window.flags &= ~WINDOW_FORCED; } @@ -2259,6 +2382,9 @@ static qboolean CG_DrawFollow( void ) { if ( !(cg.snap->ps.pm_flags & PMF_FOLLOW) ) { return qfalse; } + + CG_SetScreenPlacement(PLACE_CENTER, PLACE_TOP); + color[0] = 1; color[1] = 1; color[2] = 1; @@ -2295,6 +2421,8 @@ static void CG_DrawAmmoWarning( void ) { return; } + CG_SetScreenPlacement(PLACE_CENTER, PLACE_TOP); + if ( cg.lowAmmoWarning == 2 ) { s = "OUT OF AMMO"; } else { @@ -2322,6 +2450,8 @@ static void CG_DrawProxWarning( void ) { return; } + CG_SetScreenPlacement(PLACE_CENTER, PLACE_TOP); + if (proxTime == 0) { proxTime = cg.time; } @@ -2362,6 +2492,8 @@ static void CG_DrawWarmup( void ) { return; } + CG_SetScreenPlacement(PLACE_CENTER, PLACE_TOP); + if ( sec < 0 ) { s = "Waiting for players"; w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH; @@ -2558,6 +2690,8 @@ static void CG_Draw2D(stereoFrame_t stereoFrame) #ifdef MISSIONPACK if ( cg_drawStatus.integer ) { + CG_SetScreenPlacement(PLACE_CENTER, PLACE_BOTTOM); + Menu_PaintAll(); CG_DrawTimedMenus(); } diff --git a/code/cgame/cg_drawtools.c b/code/cgame/cg_drawtools.c index fb9e29a4f3..688f84b16e 100644 --- a/code/cgame/cg_drawtools.c +++ b/code/cgame/cg_drawtools.c @@ -23,6 +23,56 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // cg_drawtools.c -- helper functions called by cg_draw, cg_scoreboard, cg_info, etc #include "cg_local.h" +static screenPlacement_e cg_horizontalPlacement = PLACE_CENTER; +static screenPlacement_e cg_verticalPlacement = PLACE_CENTER; +static screenPlacement_e cg_lastHorizontalPlacement = PLACE_CENTER; +static screenPlacement_e cg_lastVerticalPlacement = PLACE_CENTER; + +/* +================ +CG_SetScreenPlacement +================ +*/ +void CG_SetScreenPlacement(screenPlacement_e hpos, screenPlacement_e vpos) +{ + cg_lastHorizontalPlacement = cg_horizontalPlacement; + cg_lastVerticalPlacement = cg_verticalPlacement; + + cg_horizontalPlacement = hpos; + cg_verticalPlacement = vpos; +} + +/* +================ +CG_PopScreenPlacement +================ +*/ +void CG_PopScreenPlacement(void) +{ + cg_horizontalPlacement = cg_lastHorizontalPlacement; + cg_verticalPlacement = cg_lastVerticalPlacement; +} + +/* +================ +CG_GetScreenHorizontalPlacement +================ +*/ +screenPlacement_e CG_GetScreenHorizontalPlacement(void) +{ + return cg_horizontalPlacement; +} + +/* +================ +CG_GetScreenVerticalPlacement +================ +*/ +screenPlacement_e CG_GetScreenVerticalPlacement(void) +{ + return cg_verticalPlacement; +} + /* ================ CG_AdjustFrom640 @@ -31,6 +81,7 @@ Adjusted for resolution and screen aspect ratio ================ */ void CG_AdjustFrom640( float *x, float *y, float *w, float *h ) { +#if 0 #if 0 // adjust for wide screens if ( cgs.glconfig.vidWidth * 480 > cgs.glconfig.vidHeight * 640 ) { @@ -42,6 +93,37 @@ void CG_AdjustFrom640( float *x, float *y, float *w, float *h ) { *y *= cgs.screenYScale; *w *= cgs.screenXScale; *h *= cgs.screenYScale; +#endif + + if (cg_horizontalPlacement == PLACE_STRETCH || cg_stretch.integer) { + // scale for screen sizes (not aspect correct in wide screen) + *w *= cgs.screenXScaleStretch; + *x *= cgs.screenXScaleStretch; + } else { + // scale for screen sizes + *w *= cgs.screenXScale; + *x *= cgs.screenXScale; + + if (cg_horizontalPlacement == PLACE_CENTER) { + *x += cgs.screenXBias; + } else if (cg_horizontalPlacement == PLACE_RIGHT) { + *x += cgs.screenXBias*2; + } + } + + if (cg_verticalPlacement == PLACE_STRETCH || cg_stretch.integer) { + *h *= cgs.screenYScaleStretch; + *y *= cgs.screenYScaleStretch; + } else { + *h *= cgs.screenYScale; + *y *= cgs.screenYScale; + + if (cg_verticalPlacement == PLACE_CENTER) { + *y += cgs.screenYBias; + } else if (cg_verticalPlacement == PLACE_BOTTOM) { + *y += cgs.screenYBias*2; + } + } } /* @@ -604,7 +686,7 @@ static void UI_DrawBannerString2( int x, int y, const char* str, vec4_t color ) trap_R_SetColor( color ); ax = x * cgs.screenXScale + cgs.screenXBias; - ay = y * cgs.screenYScale; + ay = y * cgs.screenYScale + cgs.screenYBias; s = str; while ( *s ) @@ -714,7 +796,7 @@ static void UI_DrawProportionalString2( int x, int y, const char* str, vec4_t co trap_R_SetColor( color ); ax = x * cgs.screenXScale + cgs.screenXBias; - ay = y * cgs.screenYScale; + ay = y * cgs.screenYScale + cgs.screenYBias; s = str; while ( *s ) diff --git a/code/cgame/cg_info.c b/code/cgame/cg_info.c index cda9027a4b..6da907f77d 100644 --- a/code/cgame/cg_info.c +++ b/code/cgame/cg_info.c @@ -164,7 +164,7 @@ void CG_DrawInformation( void ) { levelshot = trap_R_RegisterShaderNoMip( "menu/art/unknownmap" ); } trap_R_SetColor( NULL ); - CG_DrawPic( 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, levelshot ); + trap_R_DrawStretchPic( 0, 0, cgs.glconfig.vidWidth, cgs.glconfig.vidHeight, 0, 0, 1, 1, levelshot ); // blend a detail texture over it detail = trap_R_RegisterShader( "levelShotDetail" ); diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index 479498c3af..a8a790850d 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -518,6 +518,7 @@ typedef struct { // view rendering refdef_t refdef; vec3_t refdefViewAngles; // will be converted to refdef.viewaxis + float fov; // either range checked cg_fov or forced value // zoom key qboolean zoomed; @@ -995,6 +996,10 @@ typedef struct { float screenXScale; // derived from glconfig float screenYScale; float screenXBias; + float screenYBias; + float screenXScaleStretch; + float screenYScaleStretch; + int serverCommandSequence; // reliable command stream counter int processedSnapshotNum;// the number of snapshots cgame has requested @@ -1102,6 +1107,14 @@ extern vmCvar_t cg_drawCrosshair; extern vmCvar_t cg_drawCrosshairNames; extern vmCvar_t cg_drawRewards; extern vmCvar_t cg_drawTeamOverlay; +extern vmCvar_t cg_drawScores; +extern vmCvar_t cg_drawPickups; +extern vmCvar_t cg_drawWeaponBar; +extern vmCvar_t cg_drawStatusHead; +extern vmCvar_t cg_statusScale; +extern vmCvar_t cg_fovAspectAdjust; +extern vmCvar_t cg_fovGunAdjust; +extern vmCvar_t cg_stretch; extern vmCvar_t cg_teamOverlayUserinfo; extern vmCvar_t cg_crosshairX; extern vmCvar_t cg_crosshairY; @@ -1235,6 +1248,23 @@ void CG_DrawActiveFrame( int serverTime, stereoFrame_t stereoView, qboolean demo // // cg_drawtools.c // +typedef enum { + PLACE_STRETCH, + PLACE_CENTER, + + // horizontal only + PLACE_LEFT, + PLACE_RIGHT, + + // vertical only + PLACE_TOP, + PLACE_BOTTOM +} screenPlacement_e; + +void CG_SetScreenPlacement(screenPlacement_e hpos, screenPlacement_e vpos); +void CG_PopScreenPlacement(void); +screenPlacement_e CG_GetScreenHorizontalPlacement(void); +screenPlacement_e CG_GetScreenVerticalPlacement(void); void CG_AdjustFrom640( float *x, float *y, float *w, float *h ); void CG_FillRect( float x, float y, float width, float height, const float *color ); void CG_DrawPic( float x, float y, float width, float height, qhandle_t hShader ); diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index 1269e1b640..b857899880 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -108,6 +108,14 @@ vmCvar_t cg_drawAmmoWarning; vmCvar_t cg_drawCrosshair; vmCvar_t cg_drawCrosshairNames; vmCvar_t cg_drawRewards; +vmCvar_t cg_drawScores; +vmCvar_t cg_drawPickups; +vmCvar_t cg_drawWeaponBar; +vmCvar_t cg_drawStatusHead; +vmCvar_t cg_statusScale; +vmCvar_t cg_fovAspectAdjust; +vmCvar_t cg_fovGunAdjust; +vmCvar_t cg_stretch; vmCvar_t cg_crosshairSize; vmCvar_t cg_crosshairX; vmCvar_t cg_crosshairY; @@ -228,6 +236,14 @@ static cvarTable_t cvarTable[] = { { &cg_drawCrosshair, "cg_drawCrosshair", "4", CVAR_ARCHIVE }, { &cg_drawCrosshairNames, "cg_drawCrosshairNames", "1", CVAR_ARCHIVE }, { &cg_drawRewards, "cg_drawRewards", "1", CVAR_ARCHIVE }, + { &cg_drawScores, "cg_drawScores", "1", CVAR_ARCHIVE }, + { &cg_drawPickups, "cg_drawPickups", "1", CVAR_ARCHIVE }, + { &cg_drawWeaponBar, "cg_drawWeaponBar", "1", CVAR_ARCHIVE }, + { &cg_drawStatusHead, "cg_drawStatusHead", "1", CVAR_ARCHIVE }, + { &cg_statusScale, "cg_statusScale", "1", CVAR_ARCHIVE }, + { &cg_fovAspectAdjust, "cg_fovAspectAdjust", "0", CVAR_ARCHIVE }, + { &cg_fovGunAdjust, "cg_fovGunAdjust", "0", CVAR_ARCHIVE }, + { &cg_stretch, "cg_stretch", "1", CVAR_ARCHIVE }, { &cg_crosshairSize, "cg_crosshairSize", "24", CVAR_ARCHIVE }, { &cg_crosshairHealth, "cg_crosshairHealth", "1", CVAR_ARCHIVE }, { &cg_crosshairX, "cg_crosshairX", "0", CVAR_ARCHIVE }, @@ -1021,6 +1037,9 @@ static void CG_RegisterGraphics( void ) { } } + // can be used by HUD so always load it + CG_RegisterItemVisuals( 6 /* item_health_large */ ); + // wall marks cgs.media.bulletMarkShader = trap_R_RegisterShader( "gfx/damage/bullet_mrk" ); cgs.media.burnMarkShader = trap_R_RegisterShader( "gfx/damage/burn_med_mrk" ); @@ -1877,8 +1896,25 @@ void CG_Init( int serverMessageNum, int serverCommandSequence, int clientNum ) { // get the rendering configuration from the client system trap_GetGlconfig( &cgs.glconfig ); - cgs.screenXScale = cgs.glconfig.vidWidth / 640.0; - cgs.screenYScale = cgs.glconfig.vidHeight / 480.0; + cgs.screenXScaleStretch = cgs.glconfig.vidWidth * (1.0/640.0); + cgs.screenYScaleStretch = cgs.glconfig.vidHeight * (1.0/480.0); + if ( cgs.glconfig.vidWidth * 480 > cgs.glconfig.vidHeight * 640 ) { + cgs.screenXScale = cgs.glconfig.vidWidth * (1.0/640.0); + cgs.screenYScale = cgs.glconfig.vidHeight * (1.0/480.0); + // wide screen + cgs.screenXBias = 0.5 * ( cgs.glconfig.vidWidth - ( cgs.glconfig.vidHeight * (640.0/480.0) ) ); + cgs.screenXScale = cgs.screenYScale; + // no narrow screen + cgs.screenYBias = 0; + } else { + cgs.screenXScale = cgs.glconfig.vidWidth * (1.0/640.0); + cgs.screenYScale = cgs.glconfig.vidHeight * (1.0/480.0); + // narrow screen + cgs.screenYBias = 0.5 * ( cgs.glconfig.vidHeight - ( cgs.glconfig.vidWidth * (480.0/640.0) ) ); + cgs.screenYScale = cgs.screenXScale; + // no wide screen + cgs.screenXBias = 0; + } // get the gamestate from the client system trap_GetGameState( &cgs.gameState ); diff --git a/code/cgame/cg_scoreboard.c b/code/cgame/cg_scoreboard.c index ae4c60813f..5814fd30b4 100644 --- a/code/cgame/cg_scoreboard.c +++ b/code/cgame/cg_scoreboard.c @@ -272,6 +272,8 @@ qboolean CG_DrawOldScoreboard( void ) { int lineHeight; int topBorderSize, bottomBorderSize; + CG_SetScreenPlacement(PLACE_CENTER, PLACE_CENTER); + // don't draw amuthing if the menu or console is up if ( cg_paused.integer ) { cg.deferredPlayerLoading = 0; @@ -457,6 +459,8 @@ void CG_DrawTourneyScoreboard( void ) { int y; int i; + CG_SetScreenPlacement(PLACE_CENTER, PLACE_CENTER); + // request more scores regularly if ( cg.scoresRequestTime + 2000 < cg.time ) { cg.scoresRequestTime = cg.time; @@ -466,7 +470,9 @@ void CG_DrawTourneyScoreboard( void ) { // draw the dialog background color[0] = color[1] = color[2] = 0; color[3] = 1; + CG_SetScreenPlacement(PLACE_STRETCH, PLACE_STRETCH); CG_FillRect( 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, color ); + CG_PopScreenPlacement(); color[0] = 1; color[1] = 1; diff --git a/code/cgame/cg_view.c b/code/cgame/cg_view.c index 3dba5ebae2..4877e690c3 100644 --- a/code/cgame/cg_view.c +++ b/code/cgame/cg_view.c @@ -479,7 +479,7 @@ static int CG_CalcFov( void ) { if ( cg.predictedPlayerState.pm_type == PM_INTERMISSION ) { // if in intermission, use a fixed value - fov_x = 90; + cg.fov = fov_x = 90; } else { // user selectable if ( cgs.dmflags & DF_FIXED_FOV ) { @@ -494,6 +494,8 @@ static int CG_CalcFov( void ) { } } + cg.fov = fov_x; + // account for zooms zoomFov = cg_zoomFov.value; if ( zoomFov < 1 ) { @@ -517,6 +519,16 @@ static int CG_CalcFov( void ) { } } + if ( cg_fovAspectAdjust.integer ) { + // Based on LordHavoc's code for Darkplaces + // http://www.quakeworld.nu/forum/topic/53/what-does-your-qw-look-like/page/30 + const float baseAspect = 0.75f; // 3/4 + const float aspect = (float)cg.refdef.width/(float)cg.refdef.height; + const float desiredFov = fov_x; + + fov_x = atan2( tan( desiredFov*M_PI / 360.0f ) * baseAspect*aspect, 1 )*360.0f / M_PI; + } + x = cg.refdef.width / tan( fov_x / 360 * M_PI ); fov_y = atan2( cg.refdef.height, x ); fov_y = fov_y * 360 / M_PI; diff --git a/code/cgame/cg_weapons.c b/code/cgame/cg_weapons.c index 24871632e8..fd41190ca7 100644 --- a/code/cgame/cg_weapons.c +++ b/code/cgame/cg_weapons.c @@ -1359,7 +1359,7 @@ void CG_AddViewWeapon( playerState_t *ps ) { refEntity_t hand; centity_t *cent; clientInfo_t *ci; - float fovOffset; + vec3_t fovOffset; vec3_t angles; weaponInfo_t *weapon; @@ -1396,11 +1396,19 @@ void CG_AddViewWeapon( playerState_t *ps ) { return; } - // drop gun lower at higher fov - if ( cg_fov.integer > 90 ) { - fovOffset = -0.2 * ( cg_fov.integer - 90 ); - } else { - fovOffset = 0; + VectorClear(fovOffset); + + if ( cg_fovGunAdjust.integer ) { + if ( cg.fov > 90 ) { + // drop gun lower at higher fov + fovOffset[2] = -0.2 * ( cg.fov - 90 ) * cg.refdef.fov_x / cg.fov; + } else if ( cg.fov < 90 ) { + // move gun forward at lowerer fov + fovOffset[0] = -0.2 * ( cg.fov - 90 ) * cg.refdef.fov_x / cg.fov; + } + } else if ( cg_fov.integer > 90 ) { + // Q3A's auto adjust + fovOffset[2] = -0.2 * ( cg_fov.integer - 90 ); } cent = &cg.predictedPlayerEntity; // &cg_entities[cg.snap->ps.clientNum]; @@ -1412,9 +1420,9 @@ void CG_AddViewWeapon( playerState_t *ps ) { // set up gun position CG_CalculateWeaponPosition( hand.origin, angles ); - VectorMA( hand.origin, cg_gun_x.value, cg.refdef.viewaxis[0], hand.origin ); - VectorMA( hand.origin, cg_gun_y.value, cg.refdef.viewaxis[1], hand.origin ); - VectorMA( hand.origin, (cg_gun_z.value+fovOffset), cg.refdef.viewaxis[2], hand.origin ); + VectorMA( hand.origin, (cg_gun_x.value+fovOffset[0]), cg.refdef.viewaxis[0], hand.origin ); + VectorMA( hand.origin, (cg_gun_y.value+fovOffset[1]), cg.refdef.viewaxis[1], hand.origin ); + VectorMA( hand.origin, (cg_gun_z.value+fovOffset[2]), cg.refdef.viewaxis[2], hand.origin ); AnglesToAxis( angles, hand.axis ); @@ -1458,6 +1466,13 @@ void CG_DrawWeaponSelect( void ) { int x, y, w; char *name; float *color; + float markerSize, iconSize, offsetSize, charWidth, charHeight; + + CG_SetScreenPlacement(PLACE_CENTER, PLACE_BOTTOM); + + if ( cg_drawWeaponBar.value <= 0 ) { + return; + } // don't display if dead if ( cg.predictedPlayerState.stats[STAT_HEALTH] <= 0 ) { @@ -1482,9 +1497,16 @@ void CG_DrawWeaponSelect( void ) { } } - x = 320 - count * 20; + x = 320 - count * 20 * cg_drawWeaponBar.value; y = 380; + markerSize = 40 * cg_drawWeaponBar.value; + iconSize = 32 * cg_drawWeaponBar.value; + offsetSize = (markerSize - iconSize) * 0.5f; + + charWidth = BIGCHAR_WIDTH * cg_drawWeaponBar.value; + charHeight = BIGCHAR_HEIGHT * cg_drawWeaponBar.value; + for ( i = 1 ; i < MAX_WEAPONS ; i++ ) { if ( !( bits & ( 1 << i ) ) ) { continue; @@ -1493,28 +1515,28 @@ void CG_DrawWeaponSelect( void ) { CG_RegisterWeapon( i ); // draw weapon icon - CG_DrawPic( x, y, 32, 32, cg_weapons[i].weaponIcon ); + CG_DrawPic( x, y, iconSize, iconSize, cg_weapons[i].weaponIcon ); // draw selection marker if ( i == cg.weaponSelect ) { - CG_DrawPic( x-4, y-4, 40, 40, cgs.media.selectShader ); + CG_DrawPic( x-offsetSize, y-offsetSize, markerSize, markerSize, cgs.media.selectShader ); } // no ammo cross on top if ( !cg.snap->ps.ammo[ i ] ) { - CG_DrawPic( x, y, 32, 32, cgs.media.noammoShader ); + CG_DrawPic( x, y, iconSize, iconSize, cgs.media.noammoShader ); } - x += 40; + x += markerSize; } // draw the selected name if ( cg_weapons[ cg.weaponSelect ].item ) { name = cg_weapons[ cg.weaponSelect ].item->pickup_name; if ( name ) { - w = CG_DrawStrlen( name ) * BIGCHAR_WIDTH; + w = CG_DrawStrlen( name ) * charWidth; x = ( SCREEN_WIDTH - w ) / 2; - CG_DrawBigStringColor(x, y - 22, name, color); + CG_DrawStringExt(x, y - 22*cg_drawWeaponBar.value, name, color, qfalse, qtrue, charWidth, charHeight, 0 ); } } From a567f6688be033a9360fff37972982b80d4e488e Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Wed, 3 Dec 2014 00:22:02 -0600 Subject: [PATCH 03/26] Make TA voice chat head stick to left side in widescreen The HUD is drawn centered, but voice chat head should always be at left side so that console notify/chat text doesn't draw over it. Commit imported from mint-arena. --- code/cgame/cg_main.c | 3 +++ code/ui/ui_shared.c | 17 +++++++++++++++++ code/ui/ui_shared.h | 4 ++++ 3 files changed, 24 insertions(+) diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index b857899880..b8c887db14 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -1827,6 +1827,9 @@ void CG_LoadHudMenu( void ) { } CG_LoadMenus(hudSet); + + // make voice chat head stick to left side in widescreen + Menu_SetScreenPlacement( Menus_FindByName( "voiceMenu" ), PLACE_LEFT, PLACE_TOP ); } void CG_AssetCache( void ) { diff --git a/code/ui/ui_shared.c b/code/ui/ui_shared.c index 74bc77eb74..c918dd6155 100644 --- a/code/ui/ui_shared.c +++ b/code/ui/ui_shared.c @@ -4215,6 +4215,15 @@ void Menu_SetFeederSelection(menuDef_t *menu, int feeder, int index, const char } } +void Menu_SetScreenPlacement(menuDef_t *menu, screenPlacement_e hpos, screenPlacement_e vpos ) { + if ( !menu ) + return; + + menu->forceScreenPlacement = qtrue; + menu->screenHPos = hpos; + menu->screenVPos = vpos; +} + qboolean Menus_AnyFullScreenVisible(void) { int i; for (i = 0; i < menuCount; i++) { @@ -4347,6 +4356,10 @@ void Menu_Paint(menuDef_t *menu, qboolean forcePaint) { menu->window.flags |= WINDOW_FORCED; } + if (menu->forceScreenPlacement) { + CG_SetScreenPlacement( menu->screenHPos, menu->screenVPos ); + } + // draw the background if necessary if (menu->fullScreen) { // implies a background shader @@ -4370,6 +4383,10 @@ void Menu_Paint(menuDef_t *menu, qboolean forcePaint) { color[1] = 0; DC->drawRect(menu->window.rect.x, menu->window.rect.y, menu->window.rect.w, menu->window.rect.h, 1, color); } + + if (menu->forceScreenPlacement) { + CG_PopScreenPlacement(); + } } /* diff --git a/code/ui/ui_shared.h b/code/ui/ui_shared.h index e70c716f2e..9fa0d7c8cb 100644 --- a/code/ui/ui_shared.h +++ b/code/ui/ui_shared.h @@ -269,6 +269,9 @@ typedef struct { vec4_t focusColor; // focus color for items vec4_t disableColor; // focus color for items itemDef_t *items[MAX_MENUITEMS]; // items this menu contains + + qboolean forceScreenPlacement; + screenPlacement_e screenHPos, screenVPos; } menuDef_t; typedef struct { @@ -433,6 +436,7 @@ void LerpColor(vec4_t a, vec4_t b, vec4_t c, float t); void Menus_CloseAll( void ); void Menu_Paint(menuDef_t *menu, qboolean forcePaint); void Menu_SetFeederSelection(menuDef_t *menu, int feeder, int index, const char *name); +void Menu_SetScreenPlacement(menuDef_t *menu, screenPlacement_e hpos, screenPlacement_e vpos ); void Display_CacheAll( void ); void *UI_Alloc( int size ); From cf4e7555c535704e421fe18ff4177295bdd27115 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sun, 4 Jun 2017 18:25:11 -0500 Subject: [PATCH 04/26] Fix placement of Team Arena power ups in widescreen Attached Team Arena's default HUD's vertical power ups area to left or right side of screen in widescreen. Horizontal layout used by Team Arena's small hud is not affected. Commit imported from mint-arena. --- code/cgame/cg_main.c | 19 +++++++++++++++++++ code/ui/ui_shared.h | 2 ++ 2 files changed, 21 insertions(+) diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index b8c887db14..896d58f0f6 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -1830,6 +1830,25 @@ void CG_LoadHudMenu( void ) { // make voice chat head stick to left side in widescreen Menu_SetScreenPlacement( Menus_FindByName( "voiceMenu" ), PLACE_LEFT, PLACE_TOP ); + + // Make vertical power up area stick to the left or right side in widescreen. + // Team Arena has it on the right side but also handle custom huds that use left side. + { + menuDef_t *menu = Menus_FindByName( "powerup area" ); + itemDef_t *item = Menu_FindItemByName( menu, "powerupArea" ); + + if ( item && item->window.ownerDraw == CG_AREA_POWERUP && item->alignment == HUD_VERTICAL ) { + screenPlacement_e hpos; + + if ( item->window.rect.x > SCREEN_WIDTH*0.5f ) { + hpos = PLACE_RIGHT; + } else { + hpos = PLACE_LEFT; + } + + Menu_SetScreenPlacement( menu, hpos, PLACE_CENTER ); + } + } } void CG_AssetCache( void ) { diff --git a/code/ui/ui_shared.h b/code/ui/ui_shared.h index 9fa0d7c8cb..bdb57e77aa 100644 --- a/code/ui/ui_shared.h +++ b/code/ui/ui_shared.h @@ -439,6 +439,8 @@ void Menu_SetFeederSelection(menuDef_t *menu, int feeder, int index, const char void Menu_SetScreenPlacement(menuDef_t *menu, screenPlacement_e hpos, screenPlacement_e vpos ); void Display_CacheAll( void ); +itemDef_t *Menu_FindItemByName(menuDef_t *menu, const char *p); + void *UI_Alloc( int size ); void UI_InitMemory( void ); qboolean UI_OutOfMemory( void ); From 1badd133b179479c944856cf95a20b23d8ddf3cd Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sun, 4 Jun 2017 19:12:26 -0500 Subject: [PATCH 05/26] Add screenPlacement support for Team Arena menus Allow custom Team Arena HUDs to specify screen placement for each menudef. This allows parts to be attached to edge of screen in widescreen as already done by CGame for voice chat head and vertical power up area. It only affects the draw location. It's mainly for HUDs, not interactive menus. Though it could be used for menu decorations. Commit imported from mint-arena. --- code/cgame/cg_main.c | 10 +++++++--- code/ui/ui_shared.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index 896d58f0f6..31d87a1735 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -1765,6 +1765,7 @@ CG_LoadHudMenu(); void CG_LoadHudMenu( void ) { char buff[1024]; const char *hudSet; + menuDef_t *menu; cgDC.registerShaderNoMip = &trap_R_RegisterShaderNoMip; cgDC.setColor = &trap_R_SetColor; @@ -1829,12 +1830,15 @@ void CG_LoadHudMenu( void ) { CG_LoadMenus(hudSet); // make voice chat head stick to left side in widescreen - Menu_SetScreenPlacement( Menus_FindByName( "voiceMenu" ), PLACE_LEFT, PLACE_TOP ); + menu = Menus_FindByName( "voiceMenu" ); + if ( menu && !menu->forceScreenPlacement ) { + Menu_SetScreenPlacement( menu, PLACE_LEFT, PLACE_TOP ); + } // Make vertical power up area stick to the left or right side in widescreen. // Team Arena has it on the right side but also handle custom huds that use left side. - { - menuDef_t *menu = Menus_FindByName( "powerup area" ); + menu = Menus_FindByName( "powerup area" ); + if ( menu && !menu->forceScreenPlacement ) { itemDef_t *item = Menu_FindItemByName( menu, "powerupArea" ); if ( item && item->window.ownerDraw == CG_AREA_POWERUP && item->alignment == HUD_VERTICAL ) { diff --git a/code/ui/ui_shared.c b/code/ui/ui_shared.c index c918dd6155..9ea0c1066d 100644 --- a/code/ui/ui_shared.c +++ b/code/ui/ui_shared.c @@ -5786,6 +5786,49 @@ qboolean MenuParse_fadeCycle( itemDef_t *item, int handle ) { return qtrue; } +// NOTE: This only affects the draw location. It's mainly for HUDs, +// not interactive menus. Though it could be used for menu decorations. +qboolean MenuParse_screenPlacement( itemDef_t *item, int handle ) { + menuDef_t *menu = (menuDef_t*)item; + screenPlacement_e hpos, vpos; + pc_token_t token; + + if (!trap_PC_ReadToken(handle, &token)) + return qfalse; + + if (Q_stricmp(token.string, "PLACE_RIGHT") == 0) { + hpos = PLACE_RIGHT; + } else if (Q_stricmp(token.string, "PLACE_LEFT") == 0) { + hpos = PLACE_LEFT; + } else if (Q_stricmp(token.string, "PLACE_CENTER") == 0) { + hpos = PLACE_CENTER; + } else if (Q_stricmp(token.string, "PLACE_STRETCH") == 0) { + hpos = PLACE_STRETCH; + } else { + PC_SourceError(handle, "unknown screenPlacement horizontal placement %s", token.string); + return qfalse; + } + + if (!trap_PC_ReadToken(handle, &token)) + return qfalse; + + if (Q_stricmp(token.string, "PLACE_TOP") == 0) { + vpos = PLACE_RIGHT; + } else if (Q_stricmp(token.string, "PLACE_BOTTOM") == 0) { + vpos = PLACE_LEFT; + } else if (Q_stricmp(token.string, "PLACE_CENTER") == 0) { + vpos = PLACE_CENTER; + } else if (Q_stricmp(token.string, "PLACE_STRETCH") == 0) { + vpos = PLACE_STRETCH; + } else { + PC_SourceError(handle, "unknown screenPlacement vertical placement %s", token.string); + return qfalse; + } + + Menu_SetScreenPlacement( menu, hpos, vpos ); + return qtrue; +} + qboolean MenuParse_itemDef( itemDef_t *item, int handle ) { menuDef_t *menu = (menuDef_t*)item; @@ -5833,6 +5876,7 @@ keywordHash_t menuParseKeywords[] = { {"fadeClamp", MenuParse_fadeClamp, NULL}, {"fadeCycle", MenuParse_fadeCycle, NULL}, {"fadeAmount", MenuParse_fadeAmount, NULL}, + {"screenPlacement", MenuParse_screenPlacement, NULL}, {NULL, 0, NULL} }; From 9a7819eb7763a7bfcf403583af5e5b464503ea8b Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Sun, 13 May 2018 23:21:35 -0500 Subject: [PATCH 06/26] Fix compiling flexible hud --- code/cgame/cg_local.h | 4 ++++ code/cgame/cg_main.c | 3 +++ code/ui/ui_main.c | 3 +++ code/ui/ui_shared.c | 24 +++++++++++++----------- code/ui/ui_shared.h | 20 ++++++++++++++++++++ 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index a8a790850d..5bb85fe3c5 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -1248,6 +1248,9 @@ void CG_DrawActiveFrame( int serverTime, stereoFrame_t stereoView, qboolean demo // // cg_drawtools.c // +// ugly workaround for having it in cg_local.h and ui_shared.h +#ifndef HAVE_SCREEN_PLACEMENT +#define HAVE_SCREEN_PLACEMENT typedef enum { PLACE_STRETCH, PLACE_CENTER, @@ -1260,6 +1263,7 @@ typedef enum { PLACE_TOP, PLACE_BOTTOM } screenPlacement_e; +#endif void CG_SetScreenPlacement(screenPlacement_e hpos, screenPlacement_e vpos); void CG_PopScreenPlacement(void); diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index 31d87a1735..bea62d5796 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -1816,6 +1816,9 @@ void CG_LoadHudMenu( void ) { cgDC.stopCinematic = &CG_StopCinematic; cgDC.drawCinematic = &CG_DrawCinematic; cgDC.runCinematicFrame = &CG_RunCinematicFrame; + cgDC.adjustFrom640 = &CG_AdjustFrom640; + cgDC.setScreenPlacement = &CG_SetScreenPlacement; + cgDC.popScreenPlacement = &CG_PopScreenPlacement; Init_Display(&cgDC); diff --git a/code/ui/ui_main.c b/code/ui/ui_main.c index 4480d50d60..31bb1d6614 100644 --- a/code/ui/ui_main.c +++ b/code/ui/ui_main.c @@ -5149,6 +5149,9 @@ void _UI_Init( qboolean inGameLoad ) { uiInfo.uiDC.stopCinematic = &UI_StopCinematic; uiInfo.uiDC.drawCinematic = &UI_DrawCinematic; uiInfo.uiDC.runCinematicFrame = &UI_RunCinematicFrame; + uiInfo.uiDC.adjustFrom640 = &UI_AdjustFrom640; + uiInfo.uiDC.setScreenPlacement = NULL; + uiInfo.uiDC.popScreenPlacement = NULL; Init_Display(&uiInfo.uiDC); diff --git a/code/ui/ui_shared.c b/code/ui/ui_shared.c index 9ea0c1066d..f1ed435b63 100644 --- a/code/ui/ui_shared.c +++ b/code/ui/ui_shared.c @@ -3640,14 +3640,6 @@ qboolean Item_Bind_HandleKey(itemDef_t *item, int key, qboolean down) { -void AdjustFrom640(float *x, float *y, float *w, float *h) { - //*x = *x * DC->scale + DC->bias; - *x *= DC->xscale; - *y *= DC->yscale; - *w *= DC->xscale; - *h *= DC->yscale; -} - void Item_Model_Paint(itemDef_t *item) { float x, y, w, h; refdef_t refdef; @@ -3669,7 +3661,7 @@ void Item_Model_Paint(itemDef_t *item) { w = item->window.rect.w-2; h = item->window.rect.h-2; - AdjustFrom640( &x, &y, &w, &h ); + DC->adjustFrom640( &x, &y, &w, &h ); refdef.x = x; refdef.y = y; @@ -4219,6 +4211,11 @@ void Menu_SetScreenPlacement(menuDef_t *menu, screenPlacement_e hpos, screenPlac if ( !menu ) return; + if (DC->setScreenPlacement == NULL) { + Com_Printf( S_COLOR_YELLOW "WARNING: Menu_SetScreenPlacement() is only supported in CGame\n" ); + return; + } + menu->forceScreenPlacement = qtrue; menu->screenHPos = hpos; menu->screenVPos = vpos; @@ -4357,7 +4354,7 @@ void Menu_Paint(menuDef_t *menu, qboolean forcePaint) { } if (menu->forceScreenPlacement) { - CG_SetScreenPlacement( menu->screenHPos, menu->screenVPos ); + DC->setScreenPlacement( menu->screenHPos, menu->screenVPos ); } // draw the background if necessary @@ -4385,7 +4382,7 @@ void Menu_Paint(menuDef_t *menu, qboolean forcePaint) { } if (menu->forceScreenPlacement) { - CG_PopScreenPlacement(); + DC->popScreenPlacement(); } } @@ -5793,6 +5790,11 @@ qboolean MenuParse_screenPlacement( itemDef_t *item, int handle ) { screenPlacement_e hpos, vpos; pc_token_t token; + if (DC->setScreenPlacement == NULL) { + PC_SourceError(handle, "screenPlacement is only supported in HUDs"); + return qfalse; + } + if (!trap_PC_ReadToken(handle, &token)) return qfalse; diff --git a/code/ui/ui_shared.h b/code/ui/ui_shared.h index bdb57e77aa..aad6b6878a 100644 --- a/code/ui/ui_shared.h +++ b/code/ui/ui_shared.h @@ -105,6 +105,23 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #define SLIDER_THUMB_HEIGHT 20.0 #define NUM_CROSSHAIRS 10 +// ugly workaround for having it in cg_local.h and ui_shared.h +#ifndef HAVE_SCREEN_PLACEMENT +#define HAVE_SCREEN_PLACEMENT +typedef enum { + PLACE_STRETCH, + PLACE_CENTER, + + // horizontal only + PLACE_LEFT, + PLACE_RIGHT, + + // vertical only + PLACE_TOP, + PLACE_BOTTOM +} screenPlacement_e; +#endif + typedef struct { const char *command; const char *args[MAX_SCRIPT_ARGS]; @@ -369,6 +386,9 @@ typedef struct { void (*stopCinematic)(int handle); void (*drawCinematic)(int handle, float x, float y, float w, float h); void (*runCinematicFrame)(int handle); + void (*adjustFrom640)( float *x, float *y, float *w, float *h ); + void (*setScreenPlacement)( screenPlacement_e hpos, screenPlacement_e vpos ); + void (*popScreenPlacement)( void ); float yscale; float xscale; From 7f33fe4745c875e0914e2d6476e16da51a51f3b1 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Mon, 14 May 2018 00:39:27 -0500 Subject: [PATCH 07/26] Fix placement of Team Arena voiceMenu --- code/cgame/cg_newdraw.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/cgame/cg_newdraw.c b/code/cgame/cg_newdraw.c index ecb230f8c9..e0f1ec458f 100644 --- a/code/cgame/cg_newdraw.c +++ b/code/cgame/cg_newdraw.c @@ -1804,6 +1804,8 @@ int CG_ClientNumFromName(const char *p) { void CG_ShowResponseHead(void) { float x, y, w, h; + CG_SetScreenPlacement( PLACE_LEFT, PLACE_TOP ); + x = 72; y = w = h = 0; CG_AdjustFrom640( &x, &y, &w, &h ); @@ -1811,6 +1813,8 @@ void CG_ShowResponseHead(void) { Menus_OpenByName("voiceMenu"); trap_Cvar_Set("cl_conXOffset", va("%d", (int)x)); cg.voiceTime = cg.time; + + CG_PopScreenPlacement(); } void CG_RunMenuScript(char **args) { From 8525c45fad7ad0ea8083cd8e062dcc40d5e62730 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Thu, 11 Oct 2018 23:05:12 -0500 Subject: [PATCH 08/26] Enable aspect correct HUD by default --- code/cgame/cg_main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index bea62d5796..5f5a7865b2 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -243,7 +243,7 @@ static cvarTable_t cvarTable[] = { { &cg_statusScale, "cg_statusScale", "1", CVAR_ARCHIVE }, { &cg_fovAspectAdjust, "cg_fovAspectAdjust", "0", CVAR_ARCHIVE }, { &cg_fovGunAdjust, "cg_fovGunAdjust", "0", CVAR_ARCHIVE }, - { &cg_stretch, "cg_stretch", "1", CVAR_ARCHIVE }, + { &cg_stretch, "cg_stretch", "0", CVAR_ARCHIVE }, { &cg_crosshairSize, "cg_crosshairSize", "24", CVAR_ARCHIVE }, { &cg_crosshairHealth, "cg_crosshairHealth", "1", CVAR_ARCHIVE }, { &cg_crosshairX, "cg_crosshairX", "0", CVAR_ARCHIVE }, From 33044cbba578d4cf3ef9402de6b8616e8c79e6e3 Mon Sep 17 00:00:00 2001 From: Zack Middleton Date: Tue, 13 Nov 2018 04:00:49 -0600 Subject: [PATCH 09/26] Fix loadhud command not applying hud menu hacks Using loadhud command to reload HUD (instead of vid_restart) resulted in menu hacks not being applied to fix voice menu and power up area screen placement for widescreen. Ported from mint-arena. --- code/cgame/cg_consolecmds.c | 1 + code/cgame/cg_local.h | 1 + code/cgame/cg_main.c | 13 ++++++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/code/cgame/cg_consolecmds.c b/code/cgame/cg_consolecmds.c index 4c8b075e97..9a6c51ad87 100644 --- a/code/cgame/cg_consolecmds.c +++ b/code/cgame/cg_consolecmds.c @@ -134,6 +134,7 @@ static void CG_LoadHud_f( void) { } CG_LoadMenus(hudSet); + CG_HudMenuHacks(); menuScoreboard = NULL; } diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index 5bb85fe3c5..0763e94de0 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -1220,6 +1220,7 @@ void CG_UpdateCvars( void ); int CG_CrosshairPlayer( void ); int CG_LastAttacker( void ); void CG_LoadMenus(const char *menuFile); +void CG_HudMenuHacks( void ); void CG_KeyEvent(int key, qboolean down); void CG_MouseEvent(int x, int y); void CG_EventHandling(int type); diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index 5f5a7865b2..0d457deee4 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -1765,7 +1765,6 @@ CG_LoadHudMenu(); void CG_LoadHudMenu( void ) { char buff[1024]; const char *hudSet; - menuDef_t *menu; cgDC.registerShaderNoMip = &trap_R_RegisterShaderNoMip; cgDC.setColor = &trap_R_SetColor; @@ -1831,6 +1830,18 @@ void CG_LoadHudMenu( void ) { } CG_LoadMenus(hudSet); + CG_HudMenuHacks(); +} + +/* +================= +CG_HudMenuHacks +================= +*/ +void CG_HudMenuHacks( void ) { + menuDef_t *menu; + + Init_Display(&cgDC); // make voice chat head stick to left side in widescreen menu = Menus_FindByName( "voiceMenu" ); From a2bca352c0e422f04393fabbcf102da77f61d3bc Mon Sep 17 00:00:00 2001 From: milaq Date: Sat, 31 Aug 2019 13:02:10 +0200 Subject: [PATCH 10/26] Use aspect ratio FOV correction by default --- code/cgame/cg_main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index 0d457deee4..363a497500 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -241,8 +241,8 @@ static cvarTable_t cvarTable[] = { { &cg_drawWeaponBar, "cg_drawWeaponBar", "1", CVAR_ARCHIVE }, { &cg_drawStatusHead, "cg_drawStatusHead", "1", CVAR_ARCHIVE }, { &cg_statusScale, "cg_statusScale", "1", CVAR_ARCHIVE }, - { &cg_fovAspectAdjust, "cg_fovAspectAdjust", "0", CVAR_ARCHIVE }, - { &cg_fovGunAdjust, "cg_fovGunAdjust", "0", CVAR_ARCHIVE }, + { &cg_fovAspectAdjust, "cg_fovAspectAdjust", "1", CVAR_ARCHIVE }, + { &cg_fovGunAdjust, "cg_fovGunAdjust", "1", CVAR_ARCHIVE }, { &cg_stretch, "cg_stretch", "0", CVAR_ARCHIVE }, { &cg_crosshairSize, "cg_crosshairSize", "24", CVAR_ARCHIVE }, { &cg_crosshairHealth, "cg_crosshairHealth", "1", CVAR_ARCHIVE }, From 1943e6e720fccd012715cbceffd423c2b965b99a Mon Sep 17 00:00:00 2001 From: milaq Date: Sat, 12 Oct 2019 03:28:01 +0200 Subject: [PATCH 11/26] add gameversion identifier --- code/game/g_local.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/game/g_local.h b/code/game/g_local.h index ebac56c6b2..55cee8036a 100644 --- a/code/game/g_local.h +++ b/code/game/g_local.h @@ -29,7 +29,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA //================================================================== // the "gameversion" client command will print this plus compile date -#define GAMEVERSION BASEGAME +#define GAMEVERSION "mlq3a" #define BODY_QUEUE_SIZE 8 From 3a286724fe477ccefa3ff5ad5712a8d34c1d82b7 Mon Sep 17 00:00:00 2001 From: milaq Date: Sun, 13 Oct 2019 05:51:56 +0200 Subject: [PATCH 12/26] Add refined cg_forceModel handling Add cg_enemyModel and cg_teamModel cvars inspired by CPMA. If cg_forceModel is set (1), enemy models are defined by cg_enemyModel and team models by cg_teamModel. Skins (via '/') are supported but optional. Defaults are: cg_enemyModel: keel/default cg_teamModel: sarge/default --- code/cgame/cg_local.h | 3 +++ code/cgame/cg_main.c | 4 +++ code/cgame/cg_players.c | 57 ++++++++++++++++++++++------------------- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index 0763e94de0..269c81035b 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -1158,6 +1158,8 @@ extern vmCvar_t cg_teamChatTime; extern vmCvar_t cg_teamChatHeight; extern vmCvar_t cg_stats; extern vmCvar_t cg_forceModel; +extern vmCvar_t cg_enemyModel; +extern vmCvar_t cg_teamModel; extern vmCvar_t cg_buildScript; extern vmCvar_t cg_paused; extern vmCvar_t cg_blood; @@ -1304,6 +1306,7 @@ void CG_DrawTopBottom(float x, float y, float w, float h, float size); extern int sortedTeamPlayers[TEAM_MAXOVERLAY]; extern int numSortedTeamPlayers; extern int drawTeamOverlayModificationCount; +extern int forceModelModificationCount; extern char systemChat[256]; extern char teamChat1[256]; extern char teamChat2[256]; diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index 363a497500..2dfcdb2fb9 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -158,6 +158,8 @@ vmCvar_t cg_teamChatHeight; vmCvar_t cg_stats; vmCvar_t cg_buildScript; vmCvar_t cg_forceModel; +vmCvar_t cg_enemyModel; +vmCvar_t cg_teamModel; vmCvar_t cg_paused; vmCvar_t cg_blood; vmCvar_t cg_predictItems; @@ -281,6 +283,8 @@ static cvarTable_t cvarTable[] = { { &cg_teamChatTime, "cg_teamChatTime", "3000", CVAR_ARCHIVE }, { &cg_teamChatHeight, "cg_teamChatHeight", "0", CVAR_ARCHIVE }, { &cg_forceModel, "cg_forceModel", "0", CVAR_ARCHIVE }, + { &cg_enemyModel, "cg_enemyModel", "keel/default", CVAR_ARCHIVE }, + { &cg_teamModel, "cg_teamModel", "sarge/default", CVAR_ARCHIVE }, { &cg_predictItems, "cg_predictItems", "1", CVAR_ARCHIVE }, #ifdef MISSIONPACK { &cg_deferPlayers, "cg_deferPlayers", "0", CVAR_ARCHIVE }, diff --git a/code/cgame/cg_players.c b/code/cgame/cg_players.c index 82cfafdcc7..8a048cc22c 100644 --- a/code/cgame/cg_players.c +++ b/code/cgame/cg_players.c @@ -947,29 +947,35 @@ void CG_NewClientInfo( int clientNum ) { v = Info_ValueForKey( configstring, "g_blueteam" ); Q_strncpyz(newInfo.blueTeam, v, MAX_TEAMNAME); + if ( cg_forceModel.integer && cgs.gametype >= GT_TEAM && + cg.clientNum == clientNum && newInfo.team != ci->team ) { + // check whether player switched teams to update player models + // if cg_forceModel is set + forceModelModificationCount--; + } + // model v = Info_ValueForKey( configstring, "model" ); - if ( cg_forceModel.integer ) { + if ( cg_forceModel.integer && cg.clientNum != clientNum && newInfo.team != TEAM_SPECTATOR ) { // forcemodel makes everyone use a single model // to prevent load hitches char modelStr[MAX_QPATH]; char *skin; - if( cgs.gametype >= GT_TEAM ) { - Q_strncpyz( newInfo.modelName, DEFAULT_TEAM_MODEL, sizeof( newInfo.modelName ) ); - Q_strncpyz( newInfo.skinName, "default", sizeof( newInfo.skinName ) ); + if ( cgs.gametype >= GT_TEAM && cgs.clientinfo[cg.clientNum].team == newInfo.team ) { + trap_Cvar_VariableStringBuffer( "cg_teamModel", modelStr, sizeof( modelStr ) ); } else { - trap_Cvar_VariableStringBuffer( "model", modelStr, sizeof( modelStr ) ); - if ( ( skin = strchr( modelStr, '/' ) ) == NULL) { - skin = "default"; - } else { - *skin++ = 0; - } - - Q_strncpyz( newInfo.skinName, skin, sizeof( newInfo.skinName ) ); - Q_strncpyz( newInfo.modelName, modelStr, sizeof( newInfo.modelName ) ); + trap_Cvar_VariableStringBuffer( "cg_enemyModel", modelStr, sizeof( modelStr ) ); + } + if ( ( skin = strchr( modelStr, '/' ) ) == NULL) { + skin = "default"; + } else { + *skin++ = 0; } + Q_strncpyz( newInfo.skinName, skin, sizeof( newInfo.skinName ) ); + Q_strncpyz( newInfo.modelName, modelStr, sizeof( newInfo.modelName ) ); + if ( cgs.gametype >= GT_TEAM ) { // keep skin name slash = strchr( v, '/' ); @@ -993,27 +999,26 @@ void CG_NewClientInfo( int clientNum ) { // head model v = Info_ValueForKey( configstring, "hmodel" ); - if ( cg_forceModel.integer ) { + if ( cg_forceModel.integer && cg.clientNum != clientNum && newInfo.team != TEAM_SPECTATOR ) { // forcemodel makes everyone use a single model // to prevent load hitches char modelStr[MAX_QPATH]; char *skin; - if( cgs.gametype >= GT_TEAM ) { - Q_strncpyz( newInfo.headModelName, DEFAULT_TEAM_HEAD, sizeof( newInfo.headModelName ) ); - Q_strncpyz( newInfo.headSkinName, "default", sizeof( newInfo.headSkinName ) ); + if ( cgs.gametype >= GT_TEAM && cgs.clientinfo[cg.clientNum].team == newInfo.team ) { + trap_Cvar_VariableStringBuffer( "cg_teamModel", modelStr, sizeof( modelStr ) ); } else { - trap_Cvar_VariableStringBuffer( "headmodel", modelStr, sizeof( modelStr ) ); - if ( ( skin = strchr( modelStr, '/' ) ) == NULL) { - skin = "default"; - } else { - *skin++ = 0; - } - - Q_strncpyz( newInfo.headSkinName, skin, sizeof( newInfo.headSkinName ) ); - Q_strncpyz( newInfo.headModelName, modelStr, sizeof( newInfo.headModelName ) ); + trap_Cvar_VariableStringBuffer( "cg_enemyModel", modelStr, sizeof( modelStr ) ); + } + if ( ( skin = strchr( modelStr, '/' ) ) == NULL) { + skin = "default"; + } else { + *skin++ = 0; } + Q_strncpyz( newInfo.headSkinName, skin, sizeof( newInfo.headSkinName ) ); + Q_strncpyz( newInfo.headModelName, modelStr, sizeof( newInfo.headModelName ) ); + if ( cgs.gametype >= GT_TEAM ) { // keep skin name slash = strchr( v, '/' ); From 835fae175600972dd28bb98b4c89d85b06ae5f65 Mon Sep 17 00:00:00 2001 From: milaq Date: Sun, 13 Oct 2019 19:18:51 +0200 Subject: [PATCH 13/26] add in mlq3a mod build stuff --- make-mlq3a.sh | 11 +++++++++++ mlq3a-readme.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ mlq3a-version | 1 + 3 files changed, 64 insertions(+) create mode 100755 make-mlq3a.sh create mode 100644 mlq3a-readme.md create mode 100644 mlq3a-version diff --git a/make-mlq3a.sh b/make-mlq3a.sh new file mode 100755 index 0000000000..1a224caec6 --- /dev/null +++ b/make-mlq3a.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +version=$(cat mlq3a-version) +commithash=$(git log --pretty=format:'%h' -n 1) +builddate=$(date +%Y%m%d) + +mkdir -p build/mlq3a +make -j$(nproc) BUILD_SERVER=0 BUILD_CLIENT=0 +pushd build/release-linux-x86_64/baseq3/; zip -r ../../mlq3a/zpak_mlq3a_v${version}.pk3 vm/; popd +cp mlq3a-readme.md build/mlq3a/zpak_mlq3a_v${version}.txt +sed -i "2 i\\\nVersion $version (${builddate}.g${commithash})" build/mlq3a/zpak_mlq3a_v${version}.txt diff --git a/mlq3a-readme.md b/mlq3a-readme.md new file mode 100644 index 0000000000..82eaf82a10 --- /dev/null +++ b/mlq3a-readme.md @@ -0,0 +1,52 @@ +# milaq's ioquake 3 additions + +Allows modern ioquake3 cgame, qagame and ui QVM integration into any Quake 3 distribution +including assorted fixes and improvements. + +See https://github.com/milaq/ioq3/tree/mlq3a for more info. + +## Changes from upstream ioquake3 + +### Integration of ZTM's Flexible HUD mod: + +See https://clover.moe/flexible-hud-for-ioq3/ + + * `cg_stretch` [0 – 1] stretch 4:3 HUD to fit screen (default: 0) + * `cg_fovAspectAdjust` [0 – 1] automatically convert 4:3 `cg_fov` values to current aspect (default: 1) + * `cg_fovGunAdjust` [0 – 1] controls gun offset based on FOV. 0 uses Q3A code, 1 uses new code (works with FOV < 90 and fixed FOV dmflags) (default: 1) + * `cg_statusScale` [0.0 – 1.0] set scale of status bar (ammo, health, head, armor, CTF flag) (default: 1) + * `cg_drawStatusHead` [0 – 2] 0 disables drawing head, 1 acts like Q3A, 2 drawing health item instead (supports 3D and 2D icons) (default: 1) + * `cg_drawPickups` [0.0 – 1.0] set scale of item pickup message (default: 1) + * `cg_drawWeaponBar` [0.0 – 1.0] set scale of weapon select popup (available only in Team Arena) (default: 1) + * `cg_drawScores` [0 – 1] toggle drawing scores in bottom right corner (no scaling support) (default: 1) + +### Add additional `dmflags` from OpenArena: + + * `64`: Instant weapon change + * `128`: Non-accelerated jumping + * `256`: Total invisibility + * `512`: Light voting + * `1024`: No self damage from weapons + + +### Add refined `cg_forceModel` handling: + +Add `cg_enemyModel` and `cg_teamModel` cvars inspired by CPMA. + +If `cg_forceModel` is set (1), enemy models are defined by `cg_enemyModel` +and team models by `cg_teamModel`. +Skins (via '/') are supported but optional. + +Defaults are: + `cg_enemyModel`: keel/default + `cg_teamModel`: sarge/default + +## Credits + + * id Software: Creating the masterpiece Quake 3 Arena + * ioquake3 contributors: Awesome engine modernization and keeping everything tidy + * Zack Middleton: ZTM's Flexible HUD + * Razor: cg_fovAspectAdjust implementation + * LordHavoc: 4:3 FOV value to widescreen formula [flexhud] + * OpenArena contributors: Additional dmflags + * CPMA/unfreeze: cg_forceModel handling inspiration diff --git a/mlq3a-version b/mlq3a-version new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/mlq3a-version @@ -0,0 +1 @@ +1 From 5314102a31969c06ac5d7c4dbe37e50872effcc9 Mon Sep 17 00:00:00 2001 From: milaq Date: Tue, 24 Dec 2019 16:04:39 +0100 Subject: [PATCH 14/26] bump mlq3a version --- mlq3a-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlq3a-version b/mlq3a-version index d00491fd7e..0cfbf08886 100644 --- a/mlq3a-version +++ b/mlq3a-version @@ -1 +1 @@ -1 +2 From c69325611896dce718e3b0d1e1057c21d9867d57 Mon Sep 17 00:00:00 2001 From: milaq Date: Fri, 31 Jan 2020 20:19:49 +0100 Subject: [PATCH 15/26] integrate unlagged Port it from the latest version 2.01. The following was changed from the default codebase: * Lag compensation is _off_ by default (g_delagHitscan) * True ping is off by default (g_truePing) * cl_timenudge clamped to -30 and 30 (instead of -50 and 50) * Lag simulation _not_ ported (cg_latentSnaps, cg_latentCmds, cg_plOut) * Bounding box _not_ ported (cg_drawBBox) * Lightning damage server option _not_ ported (g_lightningDamage) --- Makefile | 4 + code/cgame/cg_ents.c | 76 ++++- code/cgame/cg_event.c | 52 +++- code/cgame/cg_local.h | 23 +- code/cgame/cg_main.c | 29 +- code/cgame/cg_predict.c | 256 +++++++++++++++- code/cgame/cg_servercmds.c | 4 + code/cgame/cg_snapshot.c | 2 +- code/cgame/cg_unlagged.c | 235 ++++++++++++++ code/cgame/cg_weapons.c | 68 +++-- code/game/g_active.c | 97 ++++-- code/game/g_client.c | 34 +++ code/game/g_cmds.c | 2 +- code/game/g_combat.c | 3 + code/game/g_local.h | 53 +++- code/game/g_main.c | 41 ++- code/game/g_misc.c | 4 + code/game/g_missile.c | 28 +- code/game/g_unlagged.c | 605 +++++++++++++++++++++++++++++++++++++ code/game/g_weapon.c | 66 ++-- code/qcommon/q_shared.c | 22 ++ code/qcommon/q_shared.h | 1 + 22 files changed, 1590 insertions(+), 115 deletions(-) create mode 100644 code/cgame/cg_unlagged.c create mode 100644 code/game/g_unlagged.c diff --git a/Makefile b/Makefile index a2c13a3338..c652d81037 100644 --- a/Makefile +++ b/Makefile @@ -2385,6 +2385,7 @@ Q3CGOBJ_ = \ $(B)/$(BASEGAME)/cgame/cg_scoreboard.o \ $(B)/$(BASEGAME)/cgame/cg_servercmds.o \ $(B)/$(BASEGAME)/cgame/cg_snapshot.o \ + $(B)/$(BASEGAME)/cgame/cg_unlagged.o \ $(B)/$(BASEGAME)/cgame/cg_view.o \ $(B)/$(BASEGAME)/cgame/cg_weapons.o \ \ @@ -2429,6 +2430,7 @@ MPCGOBJ_ = \ $(B)/$(MISSIONPACK)/cgame/cg_scoreboard.o \ $(B)/$(MISSIONPACK)/cgame/cg_servercmds.o \ $(B)/$(MISSIONPACK)/cgame/cg_snapshot.o \ + $(B)/$(MISSIONPACK)/cgame/cg_unlagged.o \ $(B)/$(MISSIONPACK)/cgame/cg_view.o \ $(B)/$(MISSIONPACK)/cgame/cg_weapons.o \ $(B)/$(MISSIONPACK)/ui/ui_shared.o \ @@ -2483,6 +2485,7 @@ Q3GOBJ_ = \ $(B)/$(BASEGAME)/game/g_target.o \ $(B)/$(BASEGAME)/game/g_team.o \ $(B)/$(BASEGAME)/game/g_trigger.o \ + $(B)/$(BASEGAME)/game/g_unlagged.o \ $(B)/$(BASEGAME)/game/g_utils.o \ $(B)/$(BASEGAME)/game/g_weapon.o \ \ @@ -2534,6 +2537,7 @@ MPGOBJ_ = \ $(B)/$(MISSIONPACK)/game/g_target.o \ $(B)/$(MISSIONPACK)/game/g_team.o \ $(B)/$(MISSIONPACK)/game/g_trigger.o \ + $(B)/$(MISSIONPACK)/game/g_unlagged.o \ $(B)/$(MISSIONPACK)/game/g_utils.o \ $(B)/$(MISSIONPACK)/game/g_weapon.o \ \ diff --git a/code/cgame/cg_ents.c b/code/cgame/cg_ents.c index ec8b02ff0a..58dd4efe74 100644 --- a/code/cgame/cg_ents.c +++ b/code/cgame/cg_ents.c @@ -794,15 +794,8 @@ CG_CalcEntityLerpPositions =============== */ static void CG_CalcEntityLerpPositions( centity_t *cent ) { - - // if this player does not want to see extrapolated players - if ( !cg_smoothClients.integer ) { - // make sure the clients use TR_INTERPOLATE - if ( cent->currentState.number < MAX_CLIENTS ) { - cent->currentState.pos.trType = TR_INTERPOLATE; - cent->nextState.pos.trType = TR_INTERPOLATE; - } - } + // this will be set to how far forward projectiles will be extrapolated + int timeshift = 0; if ( cent->interpolate && cent->currentState.pos.trType == TR_INTERPOLATE ) { CG_InterpolateEntityPosition( cent ); @@ -817,9 +810,52 @@ static void CG_CalcEntityLerpPositions( centity_t *cent ) { return; } + // 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; + } + + // 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; + } + } + // 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 ); + 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] ); + } + } // adjust for riding a mover if it wasn't rolled into the predicted // player state @@ -1087,10 +1123,26 @@ void CG_AddPacketEntities( void ) { // lerp the non-predicted value for lightning gun origins CG_CalcEntityLerpPositions( &cg_entities[ cg.snap->ps.clientNum ] ); + 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 ); + } + } + } + // 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 ); + if ( !cg.nextSnap || cent->nextState.eType != ET_MISSILE && cent->nextState.eType != ET_GENERAL ) { + CG_AddCEntity( cent ); + } } } diff --git a/code/cgame/cg_event.c b/code/cgame/cg_event.c index 131be74217..dc4f2bcfac 100644 --- a/code/cgame/cg_event.c +++ b/code/cgame/cg_event.c @@ -992,29 +992,61 @@ void CG_EntityEvent( centity_t *cent, vec3_t position ) { VectorMA(es->origin2, 4, cg.refdef.viewaxis[1], es->origin2); } - 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 ); + // 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 + } + 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 ); + } } break; case EV_BULLET_HIT_WALL: DEBUGNAME("EV_BULLET_HIT_WALL"); - 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 + } + else { + // do the bullet, because it wasn't predicted + ByteToDir( es->eventParm, dir ); + CG_Bullet( es->pos.trBase, es->otherEntityNum, dir, qfalse, ENTITYNUM_WORLD ); + } break; case EV_BULLET_HIT_FLESH: DEBUGNAME("EV_BULLET_HIT_FLESH"); - 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 + } + else { + // do the bullet, because it wasn't predicted + CG_Bullet( es->pos.trBase, es->otherEntityNum, dir, qtrue, es->eventParm ); + } break; case EV_SHOTGUN: DEBUGNAME("EV_SHOTGUN"); - CG_ShotgunFire( es ); + // 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 & 4) ) { + // do nothing, because it was already predicted + } + else { + // do the shotgun pattern, because it wasn't predicted + CG_ShotgunFire( es ); + } break; case EV_GENERAL_SOUND: diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index 269c81035b..7a2d2ea01a 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -446,6 +446,7 @@ typedef struct { // occurs, and they will have visible effects for #define STEP_TIME or whatever msec after #define MAX_PREDICTED_EVENTS 16 +#define NUM_SAVED_STATES (CMD_BACKUP + 2) typedef struct { int clientFrame; // incremented each frame @@ -640,6 +641,11 @@ typedef struct { char testModelName[MAX_QPATH]; qboolean testGun; + int lastPredictedCommand; + int lastServerTime; + playerState_t savedPmoveStates[NUM_SAVED_STATES]; + int stateHead, stateTail; + } cg_t; @@ -1077,6 +1083,9 @@ typedef struct { // media cgMedia_t media; + // this will be set to the server's g_delagHitscan + int delagHitscan; + } cgs_t; //============================================================================== @@ -1172,7 +1181,6 @@ extern vmCvar_t cg_noVoiceChats; extern vmCvar_t cg_noVoiceText; #endif extern vmCvar_t cg_scorePlum; -extern vmCvar_t cg_smoothClients; extern vmCvar_t pmove_fixed; extern vmCvar_t pmove_msec; //extern vmCvar_t cg_pmove_fixed; @@ -1206,6 +1214,18 @@ extern vmCvar_t cg_recordSPDemoName; extern vmCvar_t cg_obeliskRespawnDelay; #endif +extern vmCvar_t cg_delag; +extern vmCvar_t cg_debugDelag; +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; + +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 ); + // // cg_main.c // @@ -1467,6 +1487,7 @@ localEntity_t *CG_MakeExplosion( vec3_t origin, vec3_t dir, // cg_snapshot.c // void CG_ProcessSnapshots( void ); +void CG_TransitionEntity( centity_t *cent ); // // cg_info.c diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index 2dfcdb2fb9..6b0b5cc921 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -174,7 +174,6 @@ vmCvar_t cg_noVoiceText; #endif vmCvar_t cg_hudFiles; vmCvar_t cg_scorePlum; -vmCvar_t cg_smoothClients; vmCvar_t pmove_fixed; //vmCvar_t cg_pmove_fixed; vmCvar_t pmove_msec; @@ -210,6 +209,14 @@ vmCvar_t cg_recordSPDemoName; vmCvar_t cg_obeliskRespawnDelay; #endif +vmCvar_t cg_delag; +vmCvar_t cg_debugDelag; +vmCvar_t cg_cmdTimeNudge; +vmCvar_t sv_fps; +vmCvar_t cg_projectileNudge; +vmCvar_t cg_optimizePrediction; +vmCvar_t cl_timeNudge; + typedef struct { vmCvar_t *vmCvar; char *cvarName; @@ -326,7 +333,6 @@ static cvarTable_t cvarTable[] = { { &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}, { &cg_cameraMode, "com_cameraMode", "0", CVAR_CHEAT}, { &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO}, @@ -340,6 +346,14 @@ static cvarTable_t cvarTable[] = { { &cg_oldRail, "cg_oldRail", "1", CVAR_ARCHIVE}, { &cg_oldRocket, "cg_oldRocket", "1", CVAR_ARCHIVE}, { &cg_oldPlasma, "cg_oldPlasma", "1", CVAR_ARCHIVE}, + { &cg_delag, "cg_delag", "1", CVAR_ARCHIVE | CVAR_USERINFO }, + { &cg_debugDelag, "cg_debugDelag", "0", CVAR_USERINFO | 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_trueLightning, "cg_trueLightning", "0.0", CVAR_ARCHIVE} // { &cg_pmove_fixed, "cg_pmove_fixed", "0", CVAR_USERINFO | CVAR_ARCHIVE } }; @@ -402,6 +416,17 @@ void CG_UpdateCvars( void ) { cvarTable_t *cv; for ( i = 0, cv = cvarTable ; i < cvarTableSize ; i++, cv++ ) { + // 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 -30 or greater than 30 doesn't actually + // do anything more than -30 or 30 + else if ( cv->vmCvar == &cl_timeNudge ) { + CG_Cvar_ClampInt( cv->cvarName, cv->vmCvar, -30, 30 ); + } trap_Cvar_Update( cv->vmCvar ); } diff --git a/code/cgame/cg_predict.c b/code/cgame/cg_predict.c index 6ec837c8ab..fe65093191 100644 --- a/code/cgame/cg_predict.c +++ b/code/cgame/cg_predict.c @@ -382,6 +382,134 @@ static void CG_TouchTriggerPrediction( void ) { } +#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; +} + /* ================= @@ -415,6 +543,8 @@ void CG_PredictPlayerState( void ) { qboolean moved; usercmd_t oldestCmd; usercmd_t latestCmd; + int stateIndex, predictCmd; + int numPredicted = 0, numPlayedBack = 0; // debug code cg.hyperspace = qfalse; // will be set if touching a trigger_teleport @@ -499,6 +629,89 @@ void CG_PredictPlayerState( void ) { cg_pmove.pmove_fixed = pmove_fixed.integer;// | cg_pmove_fixed.integer; cg_pmove.pmove_msec = pmove_msec.integer; + // 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 ( 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; + 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; + } + // run cmds moved = qfalse; for ( cmdNum = current - CMD_BACKUP + 1 ; cmdNum <= current ; cmdNum++ ) { @@ -581,7 +794,48 @@ void CG_PredictPlayerState( void ) { cg_pmove.cmd.serverTime = ((cg_pmove.cmd.serverTime + pmove_msec.integer-1) / pmove_msec.integer) * pmove_msec.integer; } - Pmove (&cg_pmove); + if ( cg_optimizePrediction.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 + } moved = qtrue; diff --git a/code/cgame/cg_servercmds.c b/code/cgame/cg_servercmds.c index c8c54fcd68..f4c2951f3e 100644 --- a/code/cgame/cg_servercmds.c +++ b/code/cgame/cg_servercmds.c @@ -171,6 +171,10 @@ void CG_ParseServerinfo( void ) { 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); + + // 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)); } /* diff --git a/code/cgame/cg_snapshot.c b/code/cgame/cg_snapshot.c index 11b9a0678f..ef7c378ba4 100644 --- a/code/cgame/cg_snapshot.c +++ b/code/cgame/cg_snapshot.c @@ -55,7 +55,7 @@ CG_TransitionEntity cent->nextState is moved to cent->currentState and events are fired =============== */ -static void CG_TransitionEntity( centity_t *cent ) { +void CG_TransitionEntity( centity_t *cent ) { cent->currentState = cent->nextState; cent->currentValid = qtrue; diff --git a/code/cgame/cg_unlagged.c b/code/cgame/cg_unlagged.c new file mode 100644 index 0000000000..f4437a959b --- /dev/null +++ b/code/cgame/cg_unlagged.c @@ -0,0 +1,235 @@ +#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_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 --git a/code/cgame/cg_weapons.c b/code/cgame/cg_weapons.c index b914873d36..9d808498a6 100644 --- a/code/cgame/cg_weapons.c +++ b/code/cgame/cg_weapons.c @@ -977,36 +977,46 @@ static void CG_LightningBolt( centity_t *cent, vec3_t origin ) { memset( &beam, 0, sizeof( beam ) ); - // CPMA "true" lightning - if ((cent->currentState.number == cg.predictedPlayerState.clientNum) && (cg_trueLightning.value != 0)) { - vec3_t angle; - int i; - - for (i = 0; i < 3; i++) { - float a = cent->lerpAngles[i] - cg.refdefViewAngles[i]; - if (a > 180) { - a -= 360; - } - if (a < -180) { - a += 360; + // 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 { + // CPMA "true" lightning + if ((cent->currentState.number == cg.predictedPlayerState.clientNum) && (cg_trueLightning.value != 0)) { + vec3_t angle; + int i; + vec3_t viewangles; + VectorCopy( cg.predictedPlayerState.viewangles, viewangles ); + + for (i = 0; i < 3; i++) { + float a = cent->lerpAngles[i] - viewangles[i]; + if (a > 180) { + a -= 360; + } + if (a < -180) { + a += 360; + } + + angle[i] = viewangles[i] + a * (1.0 - cg_trueLightning.value); + if (angle[i] < 0) { + angle[i] += 360; + } + if (angle[i] > 360) { + angle[i] -= 360; + } } - angle[i] = cg.refdefViewAngles[i] + a * (1.0 - cg_trueLightning.value); - if (angle[i] < 0) { - angle[i] += 360; - } - if (angle[i] > 360) { - angle[i] -= 360; - } + AngleVectors(angle, forward, NULL, NULL ); + VectorCopy(cg.predictedPlayerState.origin, muzzlePoint ); + } else { + // !CPMA + AngleVectors( cent->lerpAngles, forward, NULL, NULL ); + VectorCopy(cent->lerpOrigin, muzzlePoint ); } - - AngleVectors(angle, forward, NULL, NULL ); - VectorCopy(cent->lerpOrigin, muzzlePoint ); -// VectorCopy(cg.refdef.vieworg, muzzlePoint ); - } else { - // !CPMA - AngleVectors( cent->lerpAngles, forward, NULL, NULL ); - VectorCopy(cent->lerpOrigin, muzzlePoint ); } anim = cent->currentState.legsAnim & ~ANIM_TOGGLEBIT; @@ -1752,6 +1762,8 @@ void CG_FireWeapon( centity_t *cent ) { if ( weap->ejectBrassFunc && cg_brassTime.integer > 0 ) { weap->ejectBrassFunc( cent ); } + + CG_PredictWeaponEffects( cent ); } @@ -2056,7 +2068,7 @@ Perform the same traces the server did to locate the hit splashes ================ */ -static void CG_ShotgunPattern( vec3_t origin, vec3_t origin2, int seed, int otherEntNum ) { +void CG_ShotgunPattern( vec3_t origin, vec3_t origin2, int seed, int otherEntNum ) { int i; float r, u; vec3_t end; diff --git a/code/game/g_active.c b/code/game/g_active.c index 0c84f52002..1b0f9c9782 100644 --- a/code/game/g_active.c +++ b/code/game/g_active.c @@ -777,6 +777,50 @@ void ClientThink_real( gentity_t *ent ) { // G_Printf("serverTime >>>>>\n" ); } + // 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; + + // 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; + } + + // save the command time + // attackTime will be used for backward reconciliation later (time shift) + client->attackTime = ucmd->serverTime; + + // 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; + + // make sure the true ping is over 0 - with cl_timenudge it can be less + if ( client->pers.realPing < 0 ) { + client->pers.realPing = 0; + } + + msec = ucmd->serverTime - client->ps.commandTime; // following others may result in bad times, but we still want // to check for follow toggles @@ -946,12 +990,7 @@ void ClientThink_real( gentity_t *ent ) { if ( ent->client->ps.eventSequence != oldEventSequence ) { ent->eventTime = level.time; } - 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 ); - } + BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); SendPendingPredictableEvents( &ent->client->ps ); if ( !( ent->client->ps.eFlags & EF_FIRING ) ) { @@ -1031,10 +1070,6 @@ void ClientThink( int clientNum ) { ent = g_entities + clientNum; trap_GetUsercmd( clientNum, &ent->client->pers.cmd ); - // 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; - if ( !(ent->r.svFlags & SVF_BOT) && !g_synchronousClients.integer ) { ClientThink_real( ent ); } @@ -1110,6 +1145,7 @@ while a slow client may have multiple ClientEndFrame between ClientThink. */ void ClientEndFrame( gentity_t *ent ) { int i; + int frames; if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { SpectatorClientEndFrame( ent ); @@ -1164,25 +1200,40 @@ void ClientEndFrame( gentity_t *ent ) { // apply all the damage taken this frame P_DamageFeedback (ent); - // 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; - } - ent->client->ps.stats[STAT_HEALTH] = ent->health; // FIXME: get rid of ent->health... G_SetClientSound (ent); - // set the latest infor - if (g_smoothClients.integer) { - BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); + // set the latest information + BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); + + SendPendingPredictableEvents( &ent->client->ps ); + + // 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; } - else { - BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); + + // 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 ); } - SendPendingPredictableEvents( &ent->client->ps ); + + // store the client's position for backward reconciliation later + G_StoreHistory( ent ); // set the bit for the reachability area the client is currently in // i = trap_AAS_PointReachabilityAreaIndex( ent->client->ps.origin ); diff --git a/code/game/g_client.c b/code/game/g_client.c index c6a0e87484..be025a5113 100644 --- a/code/game/g_client.c +++ b/code/game/g_client.c @@ -734,6 +734,27 @@ void ClientUserinfoChanged( int clientNum ) { client->pers.predictItemPickup = qtrue; } + // 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; + } + // set name Q_strncpyz ( oldname, client->pers.netname, sizeof( oldname ) ); s = Info_ValueForKey (userinfo, "name"); @@ -976,6 +997,14 @@ char *ClientConnect( int clientNum, qboolean firstTime, qboolean isBot ) { // if ( !client->areabits ) // client->areabits = G_Alloc( (trap_AAS_PointReachabilityAreaIndex( NULL ) + 7) / 8 ); + // announce backwards reconciliation + if ( g_delagHitscan.integer ) { + trap_SendServerCommand( clientNum, "print \"Full lag compensation is ON!\n\"" ); + } + else { + trap_SendServerCommand( clientNum, "print \"Full lag compensation is OFF!\n\"" ); + } + return NULL; } @@ -1104,6 +1133,11 @@ void ClientSpawn(gentity_t *ent) { flags = ent->client->ps.eFlags & (EF_TELEPORT_BIT | EF_VOTED | EF_TEAMVOTED); flags ^= EF_TELEPORT_BIT; + // 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; + // clear everything but the persistant data saved = client->pers; diff --git a/code/game/g_cmds.c b/code/game/g_cmds.c index f349a1bc42..6f5f3ab398 100644 --- a/code/game/g_cmds.c +++ b/code/game/g_cmds.c @@ -60,7 +60,7 @@ void DeathmatchScoreboardMessage( gentity_t *ent ) { if ( cl->pers.connected == CON_CONNECTING ) { ping = -1; } else { - ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + ping = cl->pers.realPing < 999 ? cl->pers.realPing : 999; } if( cl->accuracy_shots ) { diff --git a/code/game/g_combat.c b/code/game/g_combat.c index 509cfd7b2e..64f660c82f 100644 --- a/code/game/g_combat.c +++ b/code/game/g_combat.c @@ -447,6 +447,9 @@ void player_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int return; } + // make sure the body shows up in the client's current position + G_UnTimeShiftClient( self ); + // check for an almost capture CheckAlmostCapture( self, attacker ); // check for a player that almost brought in cubes diff --git a/code/game/g_local.h b/code/game/g_local.h index 55cee8036a..03b5228747 100644 --- a/code/game/g_local.h +++ b/code/game/g_local.h @@ -229,6 +229,7 @@ typedef struct { // #define MAX_NETNAME 36 #define MAX_VOTE_COUNT 3 +#define NUM_PING_SAMPLES 64 // client data that stays across multiple respawns, but is cleared // on each level change or team change at ClientBegin() @@ -246,8 +247,23 @@ typedef struct { int voteCount; // to prevent people from constantly calling votes int teamVoteCount; // to prevent people from constantly calling votes qboolean teamInfo; // send team overlay updates? + // these correspond with variables in the userinfo string + int delag; + int debugDelag; + int cmdTimeNudge; + int realPing; + int pingsamples[NUM_PING_SAMPLES]; + int samplehead; } clientPersistant_t; +#define NUM_CLIENT_HISTORY 17 + +// everything we need to know to backward reconcile +typedef struct { + vec3_t mins, maxs; + vec3_t currentOrigin; + int leveltime; +} clientHistory_t; // this structure is cleared on each ClientSpawn(), // except for 'client->pers' and 'client->sess' @@ -263,9 +279,6 @@ struct gclient_s { qboolean noclip; - int lastCmdTime; // level.time of last usercmd_t, for EF_CONNECTION - // we can't just use pers.lastCommand.time, because - // of the g_sycronousclients case int buttons; int oldbuttons; int latched_buttons; @@ -317,6 +330,22 @@ struct gclient_s { #endif char *areabits; + + // the serverTime the button was pressed + // (stored before pmove_fixed changes serverTime) + int attackTime; + // the head of the history queue + int historyHead; + // the history queue + clientHistory_t history[NUM_CLIENT_HISTORY]; + // the client's saved position + clientHistory_t saved; // used to restore after time shift + // an approximation of the actual server time we received this + // command (not in 50ms increments) + int frameOffset; + + // the last frame number we got an update from this client + int lastUpdateFrame; }; @@ -407,6 +436,9 @@ typedef struct { #ifdef MISSIONPACK int portalSequence; #endif + + // actual time this server frame started + int frameStartTime; } level_locals_t; @@ -551,11 +583,21 @@ void DropPortalDestination( gentity_t *ent ); // qboolean LogAccuracyHit( gentity_t *target, gentity_t *attacker ); void CalcMuzzlePoint ( gentity_t *ent, vec3_t forward, vec3_t right, vec3_t up, vec3_t muzzlePoint ); -void SnapVectorTowards( vec3_t v, vec3_t to ); qboolean CheckGauntletAttack( gentity_t *ent ); void Weapon_HookFree (gentity_t *ent); void Weapon_HookThink (gentity_t *ent); +// +// g_unlagged.c +// +void G_ResetHistory( gentity_t *ent ); +void G_StoreHistory( gentity_t *ent ); +void G_TimeShiftAllClients( int time, gentity_t *skip ); +void G_UnTimeShiftAllClients( gentity_t *skip ); +void G_DoTimeShiftFor( gentity_t *ent ); +void G_UndoTimeShiftFor( gentity_t *ent ); +void G_UnTimeShiftClient( gentity_t *client ); +void G_PredictPlayerMove( gentity_t *ent, float frametime ); // // g_client.c @@ -745,6 +787,9 @@ extern vmCvar_t g_enableBreath; extern vmCvar_t g_singlePlayer; extern vmCvar_t g_proxMineTimeout; extern vmCvar_t g_localTeamPref; +extern vmCvar_t g_delagHitscan; +extern vmCvar_t g_truePing; +extern vmCvar_t sv_fps; void trap_Print( const char *text ); void trap_Error( const char *text ) __attribute__((noreturn)); diff --git a/code/game/g_main.c b/code/game/g_main.c index d1a280269d..c442165769 100644 --- a/code/game/g_main.c +++ b/code/game/g_main.c @@ -95,6 +95,9 @@ vmCvar_t g_enableDust; vmCvar_t g_enableBreath; vmCvar_t g_proxMineTimeout; #endif +vmCvar_t g_delagHitscan; +vmCvar_t g_truePing; +vmCvar_t sv_fps; static cvarTable_t gameCvarTable[] = { // don't override the cheat state set by the system @@ -177,6 +180,11 @@ static cvarTable_t gameCvarTable[] = { { &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0, qfalse}, { &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse}, + { &g_delagHitscan, "g_delagHitscan", "0", CVAR_ARCHIVE | CVAR_SERVERINFO, 0, qtrue }, + { &g_truePing, "g_truePing", "0", CVAR_ARCHIVE, 0, qtrue }, + // it's CVAR_SYSTEMINFO so the client's sv_fps will be automagically set to its value + { &sv_fps, "sv_fps", "20", CVAR_SYSTEMINFO | CVAR_ARCHIVE, 0, qfalse }, + { &g_rankings, "g_rankings", "0", 0, 0, qfalse}, { &g_localTeamPref, "g_localTeamPref", "", 0, 0, qfalse } @@ -1854,11 +1862,6 @@ void G_RunFrame( int levelTime ) { continue; } - if ( ent->s.eType == ET_MISSILE ) { - G_RunMissile( ent ); - continue; - } - if ( ent->s.eType == ET_ITEM || ent->physicsObject ) { G_RunItem( ent ); continue; @@ -1877,6 +1880,29 @@ void G_RunFrame( int levelTime ) { G_RunThink( ent ); } + // NOW run the missiles, with all players backward-reconciled + // to the positions they were in exactly 50ms ago, at the end + // of the last server frame + G_TimeShiftAllClients( level.previousTime, NULL ); + + ent = &g_entities[0]; + for (i=0 ; iinuse ) { + continue; + } + + // temporary entities don't think + if ( ent->freeAfterEvent ) { + continue; + } + + if ( ent->s.eType == ET_MISSILE ) { + G_RunMissile( ent ); + } + } + + G_UnTimeShiftAllClients( NULL ); + // perform final fixups on the players ent = &g_entities[0]; for (i=0 ; i < level.maxclients ; i++, ent++ ) { @@ -1910,4 +1936,9 @@ void G_RunFrame( int levelTime ) { } trap_Cvar_Set("g_listEntity", "0"); } + + // record the time at the end of this frame - it should be about + // the time the next frame begins - when the server starts + // accepting commands from connected clients + level.frameStartTime = trap_Milliseconds(); } diff --git a/code/game/g_misc.c b/code/game/g_misc.c index 4a16d33f37..a0b4da95c5 100644 --- a/code/game/g_misc.c +++ b/code/game/g_misc.c @@ -107,6 +107,10 @@ void TeleportPlayer( gentity_t *player, vec3_t origin, vec3_t angles ) { } // toggle the teleport bit so the client knows to not lerp player->client->ps.eFlags ^= EF_TELEPORT_BIT; + + // we don't want players being backward-reconciled back through teleporters + G_ResetHistory( player ); + // kill anything at the destination if ( player->client->sess.sessionTeam != TEAM_SPECTATOR ) { G_KillBox (player); diff --git a/code/game/g_missile.c b/code/game/g_missile.c index 3a138b959a..c6c220a4fb 100644 --- a/code/game/g_missile.c +++ b/code/game/g_missile.c @@ -532,6 +532,8 @@ gentity_t *fire_plasma (gentity_t *self, vec3_t start, vec3_t dir) { bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; bolt->s.weapon = WP_PLASMAGUN; bolt->r.ownerNum = self->s.number; + // we'll need this for nudging projectiles later + bolt->s.otherEntityNum = self->s.number; bolt->parent = self; bolt->damage = 20; bolt->splashDamage = 15; @@ -574,6 +576,8 @@ gentity_t *fire_grenade (gentity_t *self, vec3_t start, vec3_t dir) { bolt->s.weapon = WP_GRENADE_LAUNCHER; bolt->s.eFlags = EF_BOUNCE_HALF; bolt->r.ownerNum = self->s.number; + // we'll need this for nudging projectiles later + bolt->s.otherEntityNum = self->s.number; bolt->parent = self; bolt->damage = 100; bolt->splashDamage = 100; @@ -615,6 +619,8 @@ gentity_t *fire_bfg (gentity_t *self, vec3_t start, vec3_t dir) { bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; bolt->s.weapon = WP_BFG; bolt->r.ownerNum = self->s.number; + // we'll need this for nudging projectiles later + bolt->s.otherEntityNum = self->s.number; bolt->parent = self; bolt->damage = 100; bolt->splashDamage = 100; @@ -655,6 +661,8 @@ gentity_t *fire_rocket (gentity_t *self, vec3_t start, vec3_t dir) { bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; bolt->s.weapon = WP_ROCKET_LAUNCHER; bolt->r.ownerNum = self->s.number; + // we'll need this for nudging projectiles later + bolt->s.otherEntityNum = self->s.number; bolt->parent = self; bolt->damage = 100; bolt->splashDamage = 100; @@ -681,6 +689,7 @@ fire_grapple */ gentity_t *fire_grapple (gentity_t *self, vec3_t start, vec3_t dir) { gentity_t *hook; + int hooktime; VectorNormalize (dir); @@ -697,8 +706,21 @@ gentity_t *fire_grapple (gentity_t *self, vec3_t start, vec3_t dir) { hook->parent = self; hook->target_ent = NULL; + // we might want this later + hook->s.otherEntityNum = self->s.number; + + // setting the projectile base time back makes the hook's first + // step larger + if ( self->client ) { + hooktime = self->client->pers.cmd.serverTime + 50; + } + else { + hooktime = level.time - MISSILE_PRESTEP_TIME; + } + + hook->s.pos.trTime = hooktime; + hook->s.pos.trType = TR_LINEAR; - hook->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame hook->s.otherEntityNum = self->s.number; // use to match beam in client VectorCopy( start, hook->s.pos.trBase ); VectorScale( dir, 800, hook->s.pos.trDelta ); @@ -733,6 +755,8 @@ gentity_t *fire_nail( gentity_t *self, vec3_t start, vec3_t forward, vec3_t righ bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; bolt->s.weapon = WP_NAILGUN; bolt->r.ownerNum = self->s.number; + // we'll need this for nudging projectiles later + bolt->s.otherEntityNum = self->s.number; bolt->parent = self; bolt->damage = 20; bolt->methodOfDeath = MOD_NAIL; @@ -781,6 +805,8 @@ gentity_t *fire_prox( gentity_t *self, vec3_t start, vec3_t dir ) { bolt->s.weapon = WP_PROX_LAUNCHER; bolt->s.eFlags = 0; bolt->r.ownerNum = self->s.number; + // we'll need this for nudging projectiles later + bolt->s.otherEntityNum = self->s.number; bolt->parent = self; bolt->damage = 0; bolt->splashDamage = 100; diff --git a/code/game/g_unlagged.c b/code/game/g_unlagged.c new file mode 100644 index 0000000000..b1d1410c71 --- /dev/null +++ b/code/game/g_unlagged.c @@ -0,0 +1,605 @@ +#include "g_local.h" + +/* +============ +G_ResetHistory + +Clear out the given client's history (should be called when the teleport bit is flipped) +============ +*/ +void G_ResetHistory( gentity_t *ent ) { + int i, time; + + // fill up the history with data (assume the current position) + ent->client->historyHead = NUM_CLIENT_HISTORY - 1; + for ( i = ent->client->historyHead, time = level.time; i >= 0; i--, time -= 50 ) { + VectorCopy( ent->r.mins, ent->client->history[i].mins ); + VectorCopy( ent->r.maxs, ent->client->history[i].maxs ); + VectorCopy( ent->r.currentOrigin, ent->client->history[i].currentOrigin ); + ent->client->history[i].leveltime = time; + } +} + + +/* +============ +G_StoreHistory + +Keep track of where the client's been +============ +*/ +void G_StoreHistory( gentity_t *ent ) { + int head, frametime; + + frametime = level.time - level.previousTime; + + ent->client->historyHead++; + if ( ent->client->historyHead >= NUM_CLIENT_HISTORY ) { + ent->client->historyHead = 0; + } + + head = ent->client->historyHead; + + // store all the collision-detection info and the time + VectorCopy( ent->r.mins, ent->client->history[head].mins ); + VectorCopy( ent->r.maxs, ent->client->history[head].maxs ); + VectorCopy( ent->s.pos.trBase, ent->client->history[head].currentOrigin ); + SnapVector( ent->client->history[head].currentOrigin ); + ent->client->history[head].leveltime = level.time; +} + + +/* +============= +TimeShiftLerp + +Used below to interpolate between two previous vectors +Returns a vector "frac" times the distance between "start" and "end" +============= +*/ +static void TimeShiftLerp( float frac, vec3_t start, vec3_t end, vec3_t result ) { +// From CG_InterpolateEntityPosition in cg_ents.c: +/* + cent->lerpOrigin[0] = current[0] + f * ( next[0] - current[0] ); + cent->lerpOrigin[1] = current[1] + f * ( next[1] - current[1] ); + cent->lerpOrigin[2] = current[2] + f * ( next[2] - current[2] ); +*/ +// Making these exactly the same should avoid floating-point error + + result[0] = start[0] + frac * ( end[0] - start[0] ); + result[1] = start[1] + frac * ( end[1] - start[1] ); + result[2] = start[2] + frac * ( end[2] - start[2] ); +} + + +/* +================= +G_TimeShiftClient + +Move a client back to where he was at the specified "time" +================= +*/ +void G_TimeShiftClient( gentity_t *ent, int time, qboolean debug, gentity_t *debugger ) { + int j, k; + char msg[2048]; + + // this will dump out the head index, and the time for all the stored positions +/* + if ( debug ) { + char str[MAX_STRING_CHARS]; + + Com_sprintf(str, sizeof(str), "print \"head: %d, %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d\n\"", + ent->client->historyHead, + ent->client->history[0].leveltime, + ent->client->history[1].leveltime, + ent->client->history[2].leveltime, + ent->client->history[3].leveltime, + ent->client->history[4].leveltime, + ent->client->history[5].leveltime, + ent->client->history[6].leveltime, + ent->client->history[7].leveltime, + ent->client->history[8].leveltime, + ent->client->history[9].leveltime, + ent->client->history[10].leveltime, + ent->client->history[11].leveltime, + ent->client->history[12].leveltime, + ent->client->history[13].leveltime, + ent->client->history[14].leveltime, + ent->client->history[15].leveltime, + ent->client->history[16].leveltime); + + trap_SendServerCommand( debugger - g_entities, str ); + } +*/ + + // find two entries in the history whose times sandwich "time" + // assumes no two adjacent records have the same timestamp + j = k = ent->client->historyHead; + do { + if ( ent->client->history[j].leveltime <= time ) + break; + + k = j; + j--; + if ( j < 0 ) { + j = NUM_CLIENT_HISTORY - 1; + } + } + while ( j != ent->client->historyHead ); + + // if we got past the first iteration above, we've sandwiched (or wrapped) + if ( j != k ) { + // make sure it doesn't get re-saved + if ( ent->client->saved.leveltime != level.time ) { + // save the current origin and bounding box + VectorCopy( ent->r.mins, ent->client->saved.mins ); + VectorCopy( ent->r.maxs, ent->client->saved.maxs ); + VectorCopy( ent->r.currentOrigin, ent->client->saved.currentOrigin ); + ent->client->saved.leveltime = level.time; + } + + // if we haven't wrapped back to the head, we've sandwiched, so + // we shift the client's position back to where he was at "time" + if ( j != ent->client->historyHead ) { + float frac = (float)(time - ent->client->history[j].leveltime) / + (float)(ent->client->history[k].leveltime - ent->client->history[j].leveltime); + + // interpolate between the two origins to give position at time index "time" + TimeShiftLerp( frac, + ent->client->history[j].currentOrigin, ent->client->history[k].currentOrigin, + ent->r.currentOrigin ); + + // lerp these too, just for fun (and ducking) + TimeShiftLerp( frac, + ent->client->history[j].mins, ent->client->history[k].mins, + ent->r.mins ); + + TimeShiftLerp( frac, + ent->client->history[j].maxs, ent->client->history[k].maxs, + ent->r.maxs ); + + if ( debug && debugger != NULL ) { + // print some debugging stuff exactly like what the client does + + // it starts with "Rec:" to let you know it backward-reconciled + Com_sprintf( msg, sizeof(msg), + "print \"^1Rec: time: %d, j: %d, k: %d, origin: %0.2f %0.2f %0.2f\n" + "^2frac: %0.4f, origin1: %0.2f %0.2f %0.2f, origin2: %0.2f %0.2f %0.2f\n" + "^7level.time: %d, est time: %d, level.time delta: %d, est real ping: %d\n\"", + time, ent->client->history[j].leveltime, ent->client->history[k].leveltime, + ent->r.currentOrigin[0], ent->r.currentOrigin[1], ent->r.currentOrigin[2], + frac, + ent->client->history[j].currentOrigin[0], + ent->client->history[j].currentOrigin[1], + ent->client->history[j].currentOrigin[2], + ent->client->history[k].currentOrigin[0], + ent->client->history[k].currentOrigin[1], + ent->client->history[k].currentOrigin[2], + level.time, level.time + debugger->client->frameOffset, + level.time - time, level.time + debugger->client->frameOffset - time); + + trap_SendServerCommand( debugger - g_entities, msg ); + } + + // this will recalculate absmin and absmax + trap_LinkEntity( ent ); + } else { + // we wrapped, so grab the earliest + VectorCopy( ent->client->history[k].currentOrigin, ent->r.currentOrigin ); + VectorCopy( ent->client->history[k].mins, ent->r.mins ); + VectorCopy( ent->client->history[k].maxs, ent->r.maxs ); + + // this will recalculate absmin and absmax + trap_LinkEntity( ent ); + } + } + else { + // this only happens when the client is using a negative timenudge, because that + // number is added to the command time + + // print some debugging stuff exactly like what the client does + + // it starts with "No rec:" to let you know it didn't backward-reconcile + if ( debug && debugger != NULL ) { + Com_sprintf( msg, sizeof(msg), + "print \"^1No rec: time: %d, j: %d, k: %d, origin: %0.2f %0.2f %0.2f\n" + "^2frac: %0.4f, origin1: %0.2f %0.2f %0.2f, origin2: %0.2f %0.2f %0.2f\n" + "^7level.time: %d, est time: %d, level.time delta: %d, est real ping: %d\n\"", + time, level.time, level.time, + ent->r.currentOrigin[0], ent->r.currentOrigin[1], ent->r.currentOrigin[2], + 0.0f, + ent->r.currentOrigin[0], ent->r.currentOrigin[1], ent->r.currentOrigin[2], + ent->r.currentOrigin[0], ent->r.currentOrigin[1], ent->r.currentOrigin[2], + level.time, level.time + debugger->client->frameOffset, + level.time - time, level.time + debugger->client->frameOffset - time); + + trap_SendServerCommand( debugger - g_entities, msg ); + } + } +} + + +/* +===================== +G_TimeShiftAllClients + +Move ALL clients back to where they were at the specified "time", +except for "skip" +===================== +*/ +void G_TimeShiftAllClients( int time, gentity_t *skip ) { + int i; + gentity_t *ent; + qboolean debug = ( skip != NULL && skip->client && + skip->client->pers.debugDelag && skip->s.weapon == WP_RAILGUN ); + + // for every client + ent = &g_entities[0]; + for ( i = 0; i < MAX_CLIENTS; i++, ent++ ) { + if ( ent->client && ent->inuse && ent->client->sess.sessionTeam < TEAM_SPECTATOR && ent != skip ) { + G_TimeShiftClient( ent, time, debug, skip ); + } + } +} + + +/* +================ +G_DoTimeShiftFor + +Decide what time to shift everyone back to, and do it +================ +*/ +void G_DoTimeShiftFor( gentity_t *ent ) { +#ifndef MISSIONPACK + int wpflags[WP_NUM_WEAPONS] = { 0, 0, 2, 4, 0, 0, 8, 16, 0, 0, 0 }; +#else + int wpflags[WP_NUM_WEAPONS] = { 0, 0, 2, 4, 0, 0, 8, 16, 0, 0, 0, 32, 0, 64 }; +#endif + int wpflag = wpflags[ent->client->ps.weapon]; + int time; + + // don't time shift for mistakes or bots + if ( !ent->inuse || !ent->client || (ent->r.svFlags & SVF_BOT) ) { + return; + } + + // if it's enabled server-side and the client wants it or wants it for this weapon + if ( g_delagHitscan.integer && ( ent->client->pers.delag & 1 || ent->client->pers.delag & wpflag ) ) { + // do the full lag compensation, except what the client nudges + time = ent->client->attackTime + ent->client->pers.cmdTimeNudge; + } + else { + // do just 50ms + time = level.previousTime + ent->client->frameOffset; + } + + G_TimeShiftAllClients( time, ent ); +} + + +/* +=================== +G_UnTimeShiftClient + +Move a client back to where he was before the time shift +=================== +*/ +void G_UnTimeShiftClient( gentity_t *ent ) { + // if it was saved + if ( ent->client->saved.leveltime == level.time ) { + // move it back + VectorCopy( ent->client->saved.mins, ent->r.mins ); + VectorCopy( ent->client->saved.maxs, ent->r.maxs ); + VectorCopy( ent->client->saved.currentOrigin, ent->r.currentOrigin ); + ent->client->saved.leveltime = 0; + + // this will recalculate absmin and absmax + trap_LinkEntity( ent ); + } +} + + +/* +======================= +G_UnTimeShiftAllClients + +Move ALL the clients back to where they were before the time shift, +except for "skip" +======================= +*/ +void G_UnTimeShiftAllClients( gentity_t *skip ) { + int i; + gentity_t *ent; + + ent = &g_entities[0]; + for ( i = 0; i < MAX_CLIENTS; i++, ent++) { + if ( ent->client && ent->inuse && ent->client->sess.sessionTeam < TEAM_SPECTATOR && ent != skip ) { + G_UnTimeShiftClient( ent ); + } + } +} + + +/* +================== +G_UndoTimeShiftFor + +Put everyone except for this client back where they were +================== +*/ +void G_UndoTimeShiftFor( gentity_t *ent ) { + + // don't un-time shift for mistakes or bots + if ( !ent->inuse || !ent->client || (ent->r.svFlags & SVF_BOT) ) { + return; + } + + G_UnTimeShiftAllClients( ent ); +} + + +/* +=========================== +G_PredictPlayerClipVelocity + +Slide on the impacting surface +=========================== +*/ + +#define OVERCLIP 1.001f + +void G_PredictPlayerClipVelocity( vec3_t in, vec3_t normal, vec3_t out ) { + float backoff; + + // find the magnitude of the vector "in" along "normal" + backoff = DotProduct (in, normal); + + // tilt the plane a bit to avoid floating-point error issues + if ( backoff < 0 ) { + backoff *= OVERCLIP; + } else { + backoff /= OVERCLIP; + } + + // slide along + VectorMA( in, -backoff, normal, out ); +} + + +/* +======================== +G_PredictPlayerSlideMove + +Advance the given entity frametime seconds, sliding as appropriate +======================== +*/ +#define MAX_CLIP_PLANES 5 + +qboolean G_PredictPlayerSlideMove( gentity_t *ent, float frametime ) { + int bumpcount, numbumps; + vec3_t dir; + float d; + int numplanes; + vec3_t planes[MAX_CLIP_PLANES]; + vec3_t primal_velocity, velocity, origin; + vec3_t clipVelocity; + int i, j, k; + trace_t trace; + vec3_t end; + float time_left; + float into; + vec3_t endVelocity; + vec3_t endClipVelocity; + vec3_t worldUp = { 0.0f, 0.0f, 1.0f }; + + numbumps = 4; + + VectorCopy( ent->s.pos.trDelta, primal_velocity ); + VectorCopy( primal_velocity, velocity ); + VectorCopy( ent->s.pos.trBase, origin ); + + VectorCopy( velocity, endVelocity ); + + time_left = frametime; + + numplanes = 0; + + for ( bumpcount = 0; bumpcount < numbumps; bumpcount++ ) { + + // calculate position we are trying to move to + VectorMA( origin, time_left, velocity, end ); + + // see if we can make it there + trap_Trace( &trace, origin, ent->r.mins, ent->r.maxs, end, ent->s.number, ent->clipmask ); + + if (trace.allsolid) { + // entity is completely trapped in another solid + VectorClear( velocity ); + VectorCopy( origin, ent->s.pos.trBase ); + return qtrue; + } + + if (trace.fraction > 0) { + // actually covered some distance + VectorCopy( trace.endpos, origin ); + } + + if (trace.fraction == 1) { + break; // moved the entire distance + } + + time_left -= time_left * trace.fraction; + + if ( numplanes >= MAX_CLIP_PLANES ) { + // this shouldn't really happen + VectorClear( velocity ); + VectorCopy( origin, ent->s.pos.trBase ); + return qtrue; + } + + // + // if this is the same plane we hit before, nudge velocity + // out along it, which fixes some epsilon issues with + // non-axial planes + // + for ( i = 0; i < numplanes; i++ ) { + if ( DotProduct( trace.plane.normal, planes[i] ) > 0.99 ) { + VectorAdd( trace.plane.normal, velocity, velocity ); + break; + } + } + + if ( i < numplanes ) { + continue; + } + + VectorCopy( trace.plane.normal, planes[numplanes] ); + numplanes++; + + // + // modify velocity so it parallels all of the clip planes + // + + // find a plane that it enters + for ( i = 0; i < numplanes; i++ ) { + into = DotProduct( velocity, planes[i] ); + if ( into >= 0.1 ) { + continue; // move doesn't interact with the plane + } + + // slide along the plane + G_PredictPlayerClipVelocity( velocity, planes[i], clipVelocity ); + + // slide along the plane + G_PredictPlayerClipVelocity( endVelocity, planes[i], endClipVelocity ); + + // see if there is a second plane that the new move enters + for ( j = 0; j < numplanes; j++ ) { + if ( j == i ) { + continue; + } + + if ( DotProduct( clipVelocity, planes[j] ) >= 0.1 ) { + continue; // move doesn't interact with the plane + } + + // try clipping the move to the plane + G_PredictPlayerClipVelocity( clipVelocity, planes[j], clipVelocity ); + G_PredictPlayerClipVelocity( endClipVelocity, planes[j], endClipVelocity ); + + // see if it goes back into the first clip plane + if ( DotProduct( clipVelocity, planes[i] ) >= 0 ) { + continue; + } + + // slide the original velocity along the crease + CrossProduct( planes[i], planes[j], dir ); + VectorNormalize( dir ); + d = DotProduct( dir, velocity ); + VectorScale( dir, d, clipVelocity ); + + CrossProduct( planes[i], planes[j], dir ); + VectorNormalize( dir ); + d = DotProduct( dir, endVelocity ); + VectorScale( dir, d, endClipVelocity ); + + // see if there is a third plane the the new move enters + for ( k = 0; k < numplanes; k++ ) { + if ( k == i || k == j ) { + continue; + } + + if ( DotProduct( clipVelocity, planes[k] ) >= 0.1 ) { + continue; // move doesn't interact with the plane + } + + // stop dead at a tripple plane interaction + VectorClear( velocity ); + VectorCopy( origin, ent->s.pos.trBase ); + return qtrue; + } + } + + // if we have fixed all interactions, try another move + VectorCopy( clipVelocity, velocity ); + VectorCopy( endClipVelocity, endVelocity ); + break; + } + } + + VectorCopy( endVelocity, velocity ); + VectorCopy( origin, ent->s.pos.trBase ); + + return (bumpcount != 0); +} + + +/* +============================ +G_PredictPlayerStepSlideMove + +Advance the given entity frametime seconds, stepping and sliding as appropriate +============================ +*/ +#define STEPSIZE 18 + +void G_PredictPlayerStepSlideMove( gentity_t *ent, float frametime ) { + vec3_t start_o, start_v, down_o, down_v; + vec3_t down, up; + trace_t trace; + float stepSize; + + VectorCopy (ent->s.pos.trBase, start_o); + VectorCopy (ent->s.pos.trDelta, start_v); + + if ( !G_PredictPlayerSlideMove( ent, frametime ) ) { + // not clipped, so forget stepping + return; + } + + VectorCopy( ent->s.pos.trBase, down_o); + VectorCopy( ent->s.pos.trDelta, down_v); + + VectorCopy (start_o, up); + up[2] += STEPSIZE; + + // test the player position if they were a stepheight higher + trap_Trace( &trace, start_o, ent->r.mins, ent->r.maxs, up, ent->s.number, ent->clipmask ); + if ( trace.allsolid ) { + return; // can't step up + } + + stepSize = trace.endpos[2] - start_o[2]; + + // try slidemove from this position + VectorCopy( trace.endpos, ent->s.pos.trBase ); + VectorCopy( start_v, ent->s.pos.trDelta ); + + G_PredictPlayerSlideMove( ent, frametime ); + + // push down the final amount + VectorCopy( ent->s.pos.trBase, down ); + down[2] -= stepSize; + trap_Trace( &trace, ent->s.pos.trBase, ent->r.mins, ent->r.maxs, down, ent->s.number, ent->clipmask ); + if ( !trace.allsolid ) { + VectorCopy( trace.endpos, ent->s.pos.trBase ); + } + if ( trace.fraction < 1.0 ) { + G_PredictPlayerClipVelocity( ent->s.pos.trDelta, trace.plane.normal, ent->s.pos.trDelta ); + } +} + + +/* +=================== +G_PredictPlayerMove + +Advance the given entity frametime seconds, stepping and sliding as appropriate + +This is the entry point to the server-side-only prediction code +=================== +*/ +void G_PredictPlayerMove( gentity_t *ent, float frametime ) { + G_PredictPlayerStepSlideMove( ent, frametime ); +} diff --git a/code/game/g_weapon.c b/code/game/g_weapon.c index 752cb1e21e..7bab4ff808 100644 --- a/code/game/g_weapon.c +++ b/code/game/g_weapon.c @@ -131,28 +131,6 @@ MACHINEGUN ====================================================================== */ -/* -====================== -SnapVectorTowards - -Round a vector to integers for more efficient network -transmission, but make sure that it rounds towards a given point -rather than blindly truncating. This prevents it from truncating -into a wall. -====================== -*/ -void SnapVectorTowards( vec3_t v, vec3_t to ) { - int i; - - for ( i = 0 ; i < 3 ; i++ ) { - if ( to[i] <= v[i] ) { - v[i] = floor(v[i]); - } else { - v[i] = ceil(v[i]); - } - } -} - #ifdef MISSIONPACK #define CHAINGUN_SPREAD 600 #define CHAINGUN_DAMAGE 7 @@ -173,19 +151,29 @@ void Bullet_Fire (gentity_t *ent, float spread, int damage, int mod ) { gentity_t *traceEnt; int i, passent; + // we have to use something now that the client knows in advance + int seed = ent->client->attackTime % 256; + damage *= s_quadFactor; - r = random() * M_PI * 2.0f; - u = sin(r) * crandom() * spread * 16; - r = cos(r) * crandom() * spread * 16; + r = Q_random(&seed) * M_PI * 2.0f; + u = sin(r) * Q_crandom(&seed) * spread * 16; + r = cos(r) * Q_crandom(&seed) * spread * 16; + VectorMA (muzzle, 8192*16, forward, end); VectorMA (end, r, right, end); VectorMA (end, u, up, end); passent = ent->s.number; for (i = 0; i < 10; i++) { + // backward-reconcile the other clients + G_DoTimeShiftFor( ent ); trap_Trace (&tr, muzzle, NULL, NULL, end, passent, MASK_SHOT); + + // put them back + G_UndoTimeShiftFor( ent ); + if ( tr.surfaceFlags & SURF_NOIMPACT ) { return; } @@ -199,12 +187,19 @@ void Bullet_Fire (gentity_t *ent, float spread, int damage, int mod ) { if ( traceEnt->takedamage && traceEnt->client ) { tent = G_TempEntity( tr.endpos, EV_BULLET_HIT_FLESH ); tent->s.eventParm = traceEnt->s.number; + // we need the client number to determine whether or not to + // suppress this event + tent->s.clientNum = ent->s.clientNum; + if( LogAccuracyHit( traceEnt, ent ) ) { ent->client->accuracy_hits++; } } else { tent = G_TempEntity( tr.endpos, EV_BULLET_HIT_WALL ); tent->s.eventParm = DirToByte( tr.plane.normal ); + // we need the client number to determine whether or not to + // suppress this event + tent->s.clientNum = ent->s.clientNum; } tent->s.otherEntityNum = ent->s.number; @@ -331,6 +326,9 @@ void ShotgunPattern( vec3_t origin, vec3_t origin2, int seed, gentity_t *ent ) { PerpendicularVector( right, forward ); CrossProduct( forward, right, up ); + // backward-reconcile the other clients + G_DoTimeShiftFor( ent ); + // generate the "random" spread pattern for ( i = 0 ; i < DEFAULT_SHOTGUN_COUNT ; i++ ) { r = Q_crandom( &seed ) * DEFAULT_SHOTGUN_SPREAD * 16; @@ -343,6 +341,9 @@ void ShotgunPattern( vec3_t origin, vec3_t origin2, int seed, gentity_t *ent ) { ent->client->accuracy_hits++; } } + + // put them back + G_UndoTimeShiftFor( ent ); } @@ -353,7 +354,8 @@ void weapon_supershotgun_fire (gentity_t *ent) { tent = G_TempEntity( muzzle, EV_SHOTGUN ); VectorScale( forward, 4096, tent->s.origin2 ); SnapVector( tent->s.origin2 ); - tent->s.eventParm = rand() & 255; // seed for spread pattern + // this has to be something the client can predict now + tent->s.eventParm = ent->client->attackTime % 256; // seed for spread pattern tent->s.otherEntityNum = ent->s.number; ShotgunPattern( tent->s.pos.trBase, tent->s.origin2, tent->s.eventParm, ent ); @@ -453,6 +455,9 @@ void weapon_railgun_fire (gentity_t *ent) { VectorMA (muzzle, 8192, forward, end); + // backward-reconcile the other clients + G_DoTimeShiftFor( ent ); + // trace only against the solids, so the railgun will go through people unlinked = 0; hits = 0; @@ -507,6 +512,9 @@ void weapon_railgun_fire (gentity_t *ent) { unlinked++; } while ( unlinked < MAX_RAIL_HITS ); + // put them back + G_UndoTimeShiftFor( ent ); + // link back in any entities we unlinked for ( i = 0 ; i < unlinked ; i++ ) { trap_LinkEntity( unlinkedEntities[i] ); @@ -620,8 +628,14 @@ void Weapon_LightningFire( gentity_t *ent ) { for (i = 0; i < 10; i++) { VectorMA( muzzle, LIGHTNING_RANGE, forward, end ); + // backward-reconcile the other clients + G_DoTimeShiftFor( ent ); + trap_Trace( &tr, muzzle, NULL, NULL, end, passent, MASK_SHOT ); + // put them back + G_UndoTimeShiftFor( ent ); + #ifdef MISSIONPACK // if not the first trace (the lightning bounced of an invulnerability sphere) if (i) { diff --git a/code/qcommon/q_shared.c b/code/qcommon/q_shared.c index e845c28393..f82d6780f2 100644 --- a/code/qcommon/q_shared.c +++ b/code/qcommon/q_shared.c @@ -1460,3 +1460,25 @@ char *Com_SkipTokens( char *s, int numTokens, char *sep ) else return s; } + +/* +====================== +SnapVectorTowards + +Round a vector to integers for more efficient network +transmission, but make sure that it rounds towards a given point +rather than blindly truncating. This prevents it from truncating +into a wall. +====================== +*/ +void SnapVectorTowards( vec3_t v, vec3_t to ) { + int i; + + for ( i = 0 ; i < 3 ; i++ ) { + if ( to[i] <= v[i] ) { + v[i] = floor(v[i]); + } else { + v[i] = ceil(v[i]); + } + } +} diff --git a/code/qcommon/q_shared.h b/code/qcommon/q_shared.h index 86d9ac47c3..c679e77af0 100644 --- a/code/qcommon/q_shared.h +++ b/code/qcommon/q_shared.h @@ -583,6 +583,7 @@ typedef struct { #define QuatCopy(a,b) ((b)[0]=(a)[0],(b)[1]=(a)[1],(b)[2]=(a)[2],(b)[3]=(a)[3]) #define SnapVector(v) {v[0]=((int)(v[0]));v[1]=((int)(v[1]));v[2]=((int)(v[2]));} +void SnapVectorTowards( vec3_t v, vec3_t to ); // just in case you don't want to use the macros vec_t _DotProduct( const vec3_t v1, const vec3_t v2 ); void _VectorSubtract( const vec3_t veca, const vec3_t vecb, vec3_t out ); From 8bc2ce1e5ae6a2edcbeb4164e39b37d5c6fd2514 Mon Sep 17 00:00:00 2001 From: milaq Date: Fri, 31 Jan 2020 21:09:33 +0100 Subject: [PATCH 16/26] bump mlq3a version and update readme --- mlq3a-readme.md | 18 ++++++++++++++++++ mlq3a-version | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/mlq3a-readme.md b/mlq3a-readme.md index 82eaf82a10..38366addb9 100644 --- a/mlq3a-readme.md +++ b/mlq3a-readme.md @@ -41,6 +41,24 @@ Defaults are: `cg_enemyModel`: keel/default `cg_teamModel`: sarge/default +### Integrate Unlagged by Neil "haste" Toronto + +Port from the latest version 2.01 + +`cg_delag`: Enable client side lag compensation if supported by the server (enabled by default) +`g_delagHitscan`: Enable server side lag compensation (disabled by default) + +For more info consult the Unlagged documentation. + +The following was changed from the default codebase: + + * Lag compensation is _off_ by default (g_delagHitscan) + * True ping is off by default (g_truePing) + * cl_timenudge clamped to -30 and 30 (instead of -50 and 50) + * Lag simulation _not_ ported (cg_latentSnaps, cg_latentCmds, cg_plOut) + * Bounding box _not_ ported (cg_drawBBox) + * Lightning damage server option _not_ ported (g_lightningDamage) + ## Credits * id Software: Creating the masterpiece Quake 3 Arena diff --git a/mlq3a-version b/mlq3a-version index 0cfbf08886..00750edc07 100644 --- a/mlq3a-version +++ b/mlq3a-version @@ -1 +1 @@ -2 +3 From fadef35399d69732e0de8e9dff4733a0981bb9e9 Mon Sep 17 00:00:00 2001 From: milaq Date: Sun, 9 Feb 2020 21:30:42 +0100 Subject: [PATCH 17/26] Fix server not able to set client sv_fps We need that cvar for clientside prediction. In the original unlagged code CVAR_SYSTEMINFO was not set in cgame. This also fixes the "server is not allowed to change sv_fps=..." messages during server reinits. --- code/cgame/cg_main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index 6b0b5cc921..987293c3d1 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -350,7 +350,7 @@ static cvarTable_t cvarTable[] = { { &cg_debugDelag, "cg_debugDelag", "0", CVAR_USERINFO | 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 }, + { &sv_fps, "sv_fps", "20", CVAR_SYSTEMINFO }, { &cg_projectileNudge, "cg_projectileNudge", "0", CVAR_ARCHIVE }, { &cg_optimizePrediction, "cg_optimizePrediction", "1", CVAR_ARCHIVE }, { &cl_timeNudge, "cl_timeNudge", "0", CVAR_ARCHIVE }, From 773f1254ca4b4ce0b66df236693dd2c2c7003b03 Mon Sep 17 00:00:00 2001 From: milaq Date: Mon, 10 Feb 2020 01:23:14 +0100 Subject: [PATCH 18/26] Do not announce unlagged every server (re-)init --- code/game/g_client.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/code/game/g_client.c b/code/game/g_client.c index be025a5113..96d0e65c64 100644 --- a/code/game/g_client.c +++ b/code/game/g_client.c @@ -997,14 +997,6 @@ char *ClientConnect( int clientNum, qboolean firstTime, qboolean isBot ) { // if ( !client->areabits ) // client->areabits = G_Alloc( (trap_AAS_PointReachabilityAreaIndex( NULL ) + 7) / 8 ); - // announce backwards reconciliation - if ( g_delagHitscan.integer ) { - trap_SendServerCommand( clientNum, "print \"Full lag compensation is ON!\n\"" ); - } - else { - trap_SendServerCommand( clientNum, "print \"Full lag compensation is OFF!\n\"" ); - } - return NULL; } From 824b4270f60664571d0f877f7f03703e46ed29b9 Mon Sep 17 00:00:00 2001 From: milaq Date: Thu, 23 Apr 2020 18:10:09 +0200 Subject: [PATCH 19/26] bump mlq3a version --- mlq3a-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlq3a-version b/mlq3a-version index 00750edc07..b8626c4cff 100644 --- a/mlq3a-version +++ b/mlq3a-version @@ -1 +1 @@ -3 +4 From 5be2f8f6868b047a46a25e63418bd12e9ac0c277 Mon Sep 17 00:00:00 2001 From: milaq Date: Mon, 12 Apr 2021 17:25:02 +0200 Subject: [PATCH 20/26] Add option to disable taunt sounds Reusing missionpack's `cg_noTaunt` to mute taunt sounds. EV_TAUNT still happens and triggers an animation but no sound is emitted. --- code/cgame/cg_event.c | 4 +++- code/cgame/cg_local.h | 2 +- code/cgame/cg_main.c | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/code/cgame/cg_event.c b/code/cgame/cg_event.c index dc4f2bcfac..c934bfa50b 100644 --- a/code/cgame/cg_event.c +++ b/code/cgame/cg_event.c @@ -679,7 +679,9 @@ void CG_EntityEvent( centity_t *cent, vec3_t position ) { break; case EV_TAUNT: DEBUGNAME("EV_TAUNT"); - trap_S_StartSound (NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*taunt.wav" ) ); + if (cg_noTaunt.integer == 0) { + trap_S_StartSound (NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*taunt.wav" ) ); + } break; #ifdef MISSIONPACK case EV_TAUNT_YES: diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index 7a2d2ea01a..b0281b2a69 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -1193,8 +1193,8 @@ extern vmCvar_t cg_cameraMode; #ifdef MISSIONPACK extern vmCvar_t cg_smallFont; extern vmCvar_t cg_bigFont; -extern vmCvar_t cg_noTaunt; #endif +extern vmCvar_t cg_noTaunt; extern vmCvar_t cg_noProjectileTrail; extern vmCvar_t cg_oldRail; extern vmCvar_t cg_oldRocket; diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index 987293c3d1..6910d599c0 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -187,8 +187,8 @@ vmCvar_t cg_timescale; #ifdef MISSIONPACK vmCvar_t cg_smallFont; vmCvar_t cg_bigFont; -vmCvar_t cg_noTaunt; #endif +vmCvar_t cg_noTaunt; vmCvar_t cg_noProjectileTrail; vmCvar_t cg_oldRail; vmCvar_t cg_oldRocket; @@ -340,8 +340,8 @@ static cvarTable_t cvarTable[] = { #ifdef MISSIONPACK { &cg_smallFont, "ui_smallFont", "0.25", CVAR_ARCHIVE}, { &cg_bigFont, "ui_bigFont", "0.4", CVAR_ARCHIVE}, - { &cg_noTaunt, "cg_noTaunt", "0", CVAR_ARCHIVE}, #endif + { &cg_noTaunt, "cg_noTaunt", "0", CVAR_ARCHIVE}, { &cg_noProjectileTrail, "cg_noProjectileTrail", "0", CVAR_ARCHIVE}, { &cg_oldRail, "cg_oldRail", "1", CVAR_ARCHIVE}, { &cg_oldRocket, "cg_oldRocket", "1", CVAR_ARCHIVE}, From e1e44d8c4b8bf3abf337531d219225d3d08c55c1 Mon Sep 17 00:00:00 2001 From: milaq Date: Sat, 12 Jun 2021 04:34:19 +0200 Subject: [PATCH 21/26] Add `cg_noRemoteEmptyItemSound` functionality Setting this to 1 disables the no item click sound from other players. --- code/cgame/cg_event.c | 4 +++- code/cgame/cg_local.h | 1 + code/cgame/cg_main.c | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/code/cgame/cg_event.c b/code/cgame/cg_event.c index c934bfa50b..aed79239b1 100644 --- a/code/cgame/cg_event.c +++ b/code/cgame/cg_event.c @@ -367,7 +367,9 @@ static void CG_UseItem( centity_t *cent ) { switch ( itemNum ) { default: case HI_NONE: - trap_S_StartSound (NULL, es->number, CHAN_BODY, cgs.media.useNothingSound ); + if ( cg_noRemoteEmptyItemSound.integer == 0 || es->number == cg.snap->ps.clientNum ) { + trap_S_StartSound (NULL, es->number, CHAN_BODY, cgs.media.useNothingSound ); + } break; case HI_TELEPORTER: diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index b0281b2a69..58a62cbd8d 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -1196,6 +1196,7 @@ extern vmCvar_t cg_bigFont; #endif extern vmCvar_t cg_noTaunt; extern vmCvar_t cg_noProjectileTrail; +extern vmCvar_t cg_noRemoteEmptyItemSound; extern vmCvar_t cg_oldRail; extern vmCvar_t cg_oldRocket; extern vmCvar_t cg_oldPlasma; diff --git a/code/cgame/cg_main.c b/code/cgame/cg_main.c index 6910d599c0..af1e5aa04f 100644 --- a/code/cgame/cg_main.c +++ b/code/cgame/cg_main.c @@ -190,6 +190,7 @@ vmCvar_t cg_bigFont; #endif vmCvar_t cg_noTaunt; vmCvar_t cg_noProjectileTrail; +vmCvar_t cg_noRemoteEmptyItemSound; vmCvar_t cg_oldRail; vmCvar_t cg_oldRocket; vmCvar_t cg_oldPlasma; @@ -343,6 +344,7 @@ static cvarTable_t cvarTable[] = { #endif { &cg_noTaunt, "cg_noTaunt", "0", CVAR_ARCHIVE}, { &cg_noProjectileTrail, "cg_noProjectileTrail", "0", CVAR_ARCHIVE}, + { &cg_noRemoteEmptyItemSound, "cg_noRemoteEmptyItemSound", "0", CVAR_ARCHIVE}, { &cg_oldRail, "cg_oldRail", "1", CVAR_ARCHIVE}, { &cg_oldRocket, "cg_oldRocket", "1", CVAR_ARCHIVE}, { &cg_oldPlasma, "cg_oldPlasma", "1", CVAR_ARCHIVE}, From a1f706198006a80314ee5d79ab30c39034113f24 Mon Sep 17 00:00:00 2001 From: milaq Date: Fri, 18 Jun 2021 03:16:57 +0200 Subject: [PATCH 22/26] modern ping time values for server browser colors --- code/q3_ui/ui_servers2.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/q3_ui/ui_servers2.c b/code/q3_ui/ui_servers2.c index 49c3a04b9d..592b8e194c 100644 --- a/code/q3_ui/ui_servers2.c +++ b/code/q3_ui/ui_servers2.c @@ -552,10 +552,10 @@ static void ArenaServers_UpdateMenu( void ) { else if( servernodeptr->maxPing && servernodeptr->pingtime > servernodeptr->maxPing ) { pingColor = S_COLOR_BLUE; } - else if( servernodeptr->pingtime < 200 ) { + else if( servernodeptr->pingtime < 50 ) { pingColor = S_COLOR_GREEN; } - else if( servernodeptr->pingtime < 400 ) { + else if( servernodeptr->pingtime < 100 ) { pingColor = S_COLOR_YELLOW; } else { From 6744dec374026a20c4fd1acf7a067c607b201d47 Mon Sep 17 00:00:00 2001 From: milaq Date: Fri, 18 Jun 2021 18:57:15 +0200 Subject: [PATCH 23/26] trim leading and trailing whitespace from server names --- code/q3_ui/ui_servers2.c | 1 + code/qcommon/q_shared.c | 21 +++++++++++++++++++++ code/qcommon/q_shared.h | 2 ++ 3 files changed, 24 insertions(+) diff --git a/code/q3_ui/ui_servers2.c b/code/q3_ui/ui_servers2.c index 592b8e194c..f0db9e4f67 100644 --- a/code/q3_ui/ui_servers2.c +++ b/code/q3_ui/ui_servers2.c @@ -670,6 +670,7 @@ static void ArenaServers_Insert( char* adrstr, char* info, int pingtime ) Q_strncpyz( servernodeptr->hostname, Info_ValueForKey( info, "hostname"), MAX_HOSTNAMELENGTH ); Q_CleanStr( servernodeptr->hostname ); + Q_TrimWhitespaceStr(servernodeptr->hostname); Q_strupr( servernodeptr->hostname ); Q_strncpyz( servernodeptr->mapname, Info_ValueForKey( info, "mapname"), MAX_MAPNAMELENGTH ); diff --git a/code/qcommon/q_shared.c b/code/qcommon/q_shared.c index f82d6780f2..16562c5eaf 100644 --- a/code/qcommon/q_shared.c +++ b/code/qcommon/q_shared.c @@ -1010,6 +1010,27 @@ int Q_CountChar(const char *string, char tocount) return count; } +char *Q_TrimWhitespaceStr(char *string) { + int i; + int begin = 0; + int end = strlen(string) - 1; + + while (isspace((unsigned char) string[begin])) { + begin++; + } + + while ((end >= begin) && isspace((unsigned char) string[end])) { + end--; + } + + for (i = begin; i <= end; i++) { + string[i - begin] = string[i]; + } + string[i - begin] = '\0'; + + return string; +} + int QDECL Com_sprintf(char *dest, int size, const char *fmt, ...) { int len; diff --git a/code/qcommon/q_shared.h b/code/qcommon/q_shared.h index c679e77af0..949fa43a25 100644 --- a/code/qcommon/q_shared.h +++ b/code/qcommon/q_shared.h @@ -835,6 +835,8 @@ int Q_PrintStrlen( const char *string ); char *Q_CleanStr( char *string ); // Count the number of char tocount encountered in string int Q_CountChar(const char *string, char tocount); +// trims leading and trailing whitespace from string +char *Q_TrimWhitespaceStr(char *string); //============================================= From 36370ba2f7b276e62043436c58841319b10e16d1 Mon Sep 17 00:00:00 2001 From: milaq Date: Fri, 18 Jun 2021 19:31:32 +0200 Subject: [PATCH 24/26] server browser: compact layout a tad and remove useless punkbuster logo --- code/q3_ui/ui_servers2.c | 49 +++++++++++++++------------------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/code/q3_ui/ui_servers2.c b/code/q3_ui/ui_servers2.c index f0db9e4f67..190a6272e8 100644 --- a/code/q3_ui/ui_servers2.c +++ b/code/q3_ui/ui_servers2.c @@ -59,7 +59,6 @@ MULTIPLAYER MENU (SERVER BROWSER) #define ART_UNKNOWNMAP "menu/art/unknownmap" #define ART_REMOVE0 "menu/art/delete_0" #define ART_REMOVE1 "menu/art/delete_1" -#define ART_PUNKBUSTER "menu/art/pblogo" #define ID_MASTER 10 #define ID_GAMETYPE 11 @@ -243,7 +242,6 @@ typedef struct { int numfavoriteaddresses; menulist_s punkbuster; - menubitmap_s pblogo; } arenaservers_t; static arenaservers_t g_arenaservers; @@ -1362,7 +1360,7 @@ static void ArenaServers_MenuInit( void ) { g_arenaservers.banner.style = UI_CENTER; g_arenaservers.banner.color = color_white; - y = 80; + y = 64; g_arenaservers.master.generic.type = MTYPE_SPINCONTROL; g_arenaservers.master.generic.name = "Servers:"; g_arenaservers.master.generic.flags = QMF_PULSEIFFOCUS|QMF_SMALLFONT; @@ -1410,7 +1408,17 @@ static void ArenaServers_MenuInit( void ) { g_arenaservers.showempty.generic.x = 320; g_arenaservers.showempty.generic.y = y; - y += 3 * SMALLCHAR_HEIGHT; + y += SMALLCHAR_HEIGHT; + g_arenaservers.punkbuster.generic.type = MTYPE_SPINCONTROL; + g_arenaservers.punkbuster.generic.name = "Punkbuster:"; + g_arenaservers.punkbuster.generic.flags = QMF_PULSEIFFOCUS|QMF_SMALLFONT; + g_arenaservers.punkbuster.generic.callback = ArenaServers_Event; + g_arenaservers.punkbuster.generic.id = ID_PUNKBUSTER; + g_arenaservers.punkbuster.generic.x = 320; + g_arenaservers.punkbuster.generic.y = y; + g_arenaservers.punkbuster.itemnames = punkbuster_items; + + y += 2 * SMALLCHAR_HEIGHT; g_arenaservers.list.generic.type = MTYPE_SCROLLLIST; g_arenaservers.list.generic.flags = QMF_HIGHLIGHT_IF_FOCUS; g_arenaservers.list.generic.id = ID_LIST; @@ -1418,7 +1426,7 @@ static void ArenaServers_MenuInit( void ) { g_arenaservers.list.generic.x = 72; g_arenaservers.list.generic.y = y; g_arenaservers.list.width = MAX_LISTBOXWIDTH; - g_arenaservers.list.height = 11; + g_arenaservers.list.height = 12; g_arenaservers.list.itemnames = (const char **)g_arenaservers.items; for( i = 0; i < MAX_LISTBOXITEMS; i++ ) { g_arenaservers.items[i] = g_arenaservers.table[i].buff; @@ -1427,7 +1435,7 @@ static void ArenaServers_MenuInit( void ) { g_arenaservers.mappic.generic.type = MTYPE_BITMAP; g_arenaservers.mappic.generic.flags = QMF_LEFT_JUSTIFY|QMF_INACTIVE; g_arenaservers.mappic.generic.x = 72; - g_arenaservers.mappic.generic.y = 80; + g_arenaservers.mappic.generic.y = 64; g_arenaservers.mappic.width = 128; g_arenaservers.mappic.height = 96; g_arenaservers.mappic.errorpic = ART_UNKNOWNMAP; @@ -1437,7 +1445,7 @@ static void ArenaServers_MenuInit( void ) { g_arenaservers.arrows.generic.flags = QMF_LEFT_JUSTIFY|QMF_INACTIVE; g_arenaservers.arrows.generic.callback = ArenaServers_Event; g_arenaservers.arrows.generic.x = 512+48; - g_arenaservers.arrows.generic.y = 240-64+16; + g_arenaservers.arrows.generic.y = 240-64; g_arenaservers.arrows.width = 64; g_arenaservers.arrows.height = 128; @@ -1446,7 +1454,7 @@ static void ArenaServers_MenuInit( void ) { g_arenaservers.up.generic.callback = ArenaServers_Event; g_arenaservers.up.generic.id = ID_SCROLL_UP; g_arenaservers.up.generic.x = 512+48; - g_arenaservers.up.generic.y = 240-64+16; + g_arenaservers.up.generic.y = 240-64; g_arenaservers.up.width = 64; g_arenaservers.up.height = 64; g_arenaservers.up.focuspic = ART_ARROWS_UP; @@ -1456,7 +1464,7 @@ static void ArenaServers_MenuInit( void ) { g_arenaservers.down.generic.callback = ArenaServers_Event; g_arenaservers.down.generic.id = ID_SCROLL_DOWN; g_arenaservers.down.generic.x = 512+48; - g_arenaservers.down.generic.y = 240+16; + g_arenaservers.down.generic.y = 240; g_arenaservers.down.width = 64; g_arenaservers.down.height = 64; g_arenaservers.down.focuspic = ART_ARROWS_DOWN; @@ -1542,24 +1550,6 @@ static void ArenaServers_MenuInit( void ) { g_arenaservers.go.width = 128; g_arenaservers.go.height = 64; g_arenaservers.go.focuspic = ART_CONNECT1; - - g_arenaservers.punkbuster.generic.type = MTYPE_SPINCONTROL; - g_arenaservers.punkbuster.generic.name = "Punkbuster:"; - g_arenaservers.punkbuster.generic.flags = QMF_PULSEIFFOCUS|QMF_SMALLFONT; - g_arenaservers.punkbuster.generic.callback = ArenaServers_Event; - g_arenaservers.punkbuster.generic.id = ID_PUNKBUSTER; - g_arenaservers.punkbuster.generic.x = 480+32; - g_arenaservers.punkbuster.generic.y = 144; - g_arenaservers.punkbuster.itemnames = punkbuster_items; - - g_arenaservers.pblogo.generic.type = MTYPE_BITMAP; - g_arenaservers.pblogo.generic.name = ART_PUNKBUSTER; - g_arenaservers.pblogo.generic.flags = QMF_LEFT_JUSTIFY|QMF_INACTIVE; - g_arenaservers.pblogo.generic.x = 526; - g_arenaservers.pblogo.generic.y = 176; - g_arenaservers.pblogo.width = 32; - g_arenaservers.pblogo.height = 16; - g_arenaservers.pblogo.errorpic = ART_UNKNOWNMAP; Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.banner ); @@ -1568,6 +1558,7 @@ static void ArenaServers_MenuInit( void ) { Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.sortkey ); Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.showfull); Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.showempty ); + Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.punkbuster ); Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.mappic ); Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.status ); @@ -1583,9 +1574,6 @@ static void ArenaServers_MenuInit( void ) { Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.refresh ); Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.create ); Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.go ); - - Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.punkbuster ); - Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.pblogo ); ArenaServers_LoadFavorites(); @@ -1632,7 +1620,6 @@ void ArenaServers_Cache( void ) { trap_R_RegisterShaderNoMip( ART_ARROWS_UP ); trap_R_RegisterShaderNoMip( ART_ARROWS_DOWN ); trap_R_RegisterShaderNoMip( ART_UNKNOWNMAP ); - trap_R_RegisterShaderNoMip( ART_PUNKBUSTER ); } From 532e5b837a0f731821d73b5060f9a56d66fa7dab Mon Sep 17 00:00:00 2001 From: milaq Date: Sun, 9 Feb 2020 15:20:27 +0100 Subject: [PATCH 25/26] Increase zone memory Double the amount of preallocated zone memory which is needed for some recent high resolution texture packs (e.g. "Neural Upscale" by MamiyaOtaru). Increasing this makes sense now that we have https://github.com/ioquake/ioq3/commit/20a77764dd7f62101f8a3750471dab300a840858#diff-12a54363af4f3f3aaa86590d1d6b3e38 and it doesn't hurt either in times where 8GB+ of ram is _very_ common. Additionally, as stated in https://github.com/ioquake/ioq3/blob/master/code/qcommon/common.c#L1435 is is impossible to set com_zonemegs via config file and thus making it hard to ship zonemegs increasement with any mods or packs. --- code/qcommon/common.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/qcommon/common.c b/code/qcommon/common.c index 8512cc0d93..9b93b4d620 100644 --- a/code/qcommon/common.c +++ b/code/qcommon/common.c @@ -39,7 +39,7 @@ int demo_protocols[] = #define MIN_DEDICATED_COMHUNKMEGS 1 #define MIN_COMHUNKMEGS 56 #define DEF_COMHUNKMEGS 128 -#define DEF_COMZONEMEGS 24 +#define DEF_COMZONEMEGS 48 #define DEF_COMHUNKMEGS_S XSTRING(DEF_COMHUNKMEGS) #define DEF_COMZONEMEGS_S XSTRING(DEF_COMZONEMEGS) From f3622646fed77cbd0af4e91d96d7e30726224815 Mon Sep 17 00:00:00 2001 From: milaq Date: Thu, 3 Mar 2022 17:50:11 +0100 Subject: [PATCH 26/26] Serverbrowser: Sanitize server names Allowed characters: A-Z, a-z, 0-9 Additionally, a few special characters with the following rules: : After brackets or alphanumeric characters '-' and '_': After alphanumeric characters Brackets: After alphanumeric characters, spaces or at the beginning of the string This is to filter any garbage or padding characters while still allowing clan tags, country codes and the likes. --- code/q3_ui/ui_servers2.c | 1 + code/qcommon/q_shared.c | 38 ++++++++++++++++++++++++++++++++++++++ code/qcommon/q_shared.h | 2 ++ 3 files changed, 41 insertions(+) diff --git a/code/q3_ui/ui_servers2.c b/code/q3_ui/ui_servers2.c index 190a6272e8..731d876eb9 100644 --- a/code/q3_ui/ui_servers2.c +++ b/code/q3_ui/ui_servers2.c @@ -668,6 +668,7 @@ static void ArenaServers_Insert( char* adrstr, char* info, int pingtime ) Q_strncpyz( servernodeptr->hostname, Info_ValueForKey( info, "hostname"), MAX_HOSTNAMELENGTH ); Q_CleanStr( servernodeptr->hostname ); + Q_SanitizeServerName(servernodeptr->hostname); Q_TrimWhitespaceStr(servernodeptr->hostname); Q_strupr( servernodeptr->hostname ); diff --git a/code/qcommon/q_shared.c b/code/qcommon/q_shared.c index 16562c5eaf..586e568301 100644 --- a/code/qcommon/q_shared.c +++ b/code/qcommon/q_shared.c @@ -1031,6 +1031,44 @@ char *Q_TrimWhitespaceStr(char *string) { return string; } +char *Q_SanitizeServerName(char *string) { + char* d; + char* s; + int c; + int chartype = -1; + + s = string; + d = string; + while ((c = *s) != 0) { + if (Q_IsColorString(s)) { + s++; + } + else if (isalnum(c) == 1) { + *d++ = c; + chartype = 0; + } + else if ((chartype == 0 || chartype == 3) && + c == 0x20) { + *d++ = c; + chartype = 1; + } + else if (chartype == 0 && + (c == 0x2d || c == 0x5f)) { + *d++ = c; + chartype = 2; + } + else if ((chartype == -1 || chartype == 0 || chartype == 1) && + (c == 0x5b || c == 0x5d || c == 0x28 || c == 0x29)) { + *d++ = c; + chartype = 3; + } + s++; + } + *d = '\0'; + + return string; +} + int QDECL Com_sprintf(char *dest, int size, const char *fmt, ...) { int len; diff --git a/code/qcommon/q_shared.h b/code/qcommon/q_shared.h index 949fa43a25..629fe27f3a 100644 --- a/code/qcommon/q_shared.h +++ b/code/qcommon/q_shared.h @@ -837,6 +837,8 @@ char *Q_CleanStr( char *string ); int Q_CountChar(const char *string, char tocount); // trims leading and trailing whitespace from string char *Q_TrimWhitespaceStr(char *string); +// strip illegal or garbage characters from string +char *Q_SanitizeServerName(char *string); //=============================================