Counter Strike : Global Offensive Source Code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

618 lines
18 KiB

//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
//=============================================================================//
#include "cbase.h"
#include "fx_cs_shared.h"
#include "weapon_csbase.h"
#include "rumble_shared.h"
#ifndef CLIENT_DLL
#include "ilagcompensationmanager.h"
#endif
ConVar weapon_accuracy_logging( "weapon_accuracy_logging", "0", FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY | FCVAR_ARCHIVE );
ConVar steam_controller_haptics( "steam_controller_haptics", "1", FCVAR_RELEASE );
ConVar weapon_near_empty_sound( "weapon_near_empty_sound", "1", FCVAR_REPLICATED | FCVAR_CHEAT );
ConVar weapon_debug_max_inaccuracy( "weapon_debug_max_inaccuracy", "0", FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY | FCVAR_CHEAT, "Force all shots to have maximum inaccuracy" );
ConVar weapon_debug_inaccuracy_only_up( "weapon_debug_inaccuracy_only_up", "0", FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY | FCVAR_CHEAT, "Force weapon inaccuracy to be in exactly the up direction" );
ConVar snd_max_pitch_shift_inaccuracy("snd_max_pitch_shift_inaccuracy", "0.08", 0);
#ifdef CLIENT_DLL
#include "fx_impact.h"
#include "c_rumble.h"
#include "inputsystem/iinputsystem.h"
// NOTE: This has to be the last file included!
#include "tier0/memdbgon.h"
// this is a cheap ripoff from CBaseCombatWeapon::WeaponSound():
void FX_WeaponSound(
int iPlayerIndex,
uint16 nItemDefIndex,
WeaponSound_t sound_type,
const Vector &vOrigin,
const CCSWeaponInfo *pWeaponInfo,
float flSoundTime,
int nPitch )
{
// If we have some sounds from the weapon classname.txt file, play a random one of them
const char *shootsound = pWeaponInfo->aShootSounds[ sound_type ];
// Get the item definition
const CEconItemDefinition *pDef = ( nItemDefIndex > 0 ) ? GetItemSchema()->GetItemDefinition( nItemDefIndex ) : NULL;
if ( pDef )
{
const char *pszTempSound = pDef->GetWeaponReplacementSound( sound_type );
if ( pszTempSound )
{
shootsound = pszTempSound;
}
}
if ( !shootsound || !shootsound[0] )
return;
CBroadcastRecipientFilter filter; // this is client side only
if ( !te->CanPredict() )
return;
EmitSound_t params;
params.m_pSoundName = shootsound;
params.m_flSoundTime = flSoundTime;
params.m_pOrigin = &vOrigin;
params.m_pflSoundDuration = nullptr;
params.m_bWarnOnDirectWaveReference = true;
params.m_nPitch = nPitch;
if (nPitch != PITCH_NORM)
{
params.m_nFlags = params.m_nFlags | SND_OVERRIDE_PITCH;
}
CBaseEntity::EmitSound( filter, iPlayerIndex, params );
}
class CGroupedSound
{
public:
string_t m_SoundName;
Vector m_vPos;
};
CUtlVector<CGroupedSound> g_GroupedSounds;
// Called by the ImpactSound function.
void ShotgunImpactSoundGroup( const char *pSoundName, const Vector &vEndPos )
{
int i;
// Don't play the sound if it's too close to another impact sound.
for ( i=0; i < g_GroupedSounds.Count(); i++ )
{
CGroupedSound *pSound = &g_GroupedSounds[i];
if ( vEndPos.DistToSqr( pSound->m_vPos ) < 300*300 )
{
if ( Q_stricmp( pSound->m_SoundName, pSoundName ) == 0 )
return;
}
}
// Ok, play the sound and add it to the list.
CLocalPlayerFilter filter;
C_BaseEntity::EmitSound( filter, NULL, pSoundName, &vEndPos );
i = g_GroupedSounds.AddToTail();
g_GroupedSounds[i].m_SoundName = pSoundName;
g_GroupedSounds[i].m_vPos = vEndPos;
}
void StartGroupingSounds()
{
Assert( g_GroupedSounds.Count() == 0 );
SetImpactSoundRoute( ShotgunImpactSoundGroup );
}
void EndGroupingSounds()
{
g_GroupedSounds.Purge();
SetImpactSoundRoute( NULL );
}
#else
#include "te_shotgun_shot.h"
// Server doesn't play sounds anyway.
void StartGroupingSounds() {}
void EndGroupingSounds() {}
void FX_WeaponSound ( int iPlayerIndex,
uint16 nItemDefIndex,
WeaponSound_t sound_type,
const Vector &vOrigin,
const CCSWeaponInfo *pWeaponInfo, float flSoundTime, int nPitch ) {};
#endif
ConVar debug_aim_angle("debug_aim_angle", "0", FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY);
// This runs on both the client and the server.
// On the server, it only does the damage calculations.
// On the client, it does all the effects.
void FX_FireBullets(
int iPlayerIndex,
uint16 nItemDefIndex,
const Vector &vOrigin,
const QAngle &vAngles,
CSWeaponID iWeaponID,
int iMode,
int iSeed,
float fInaccuracy,
float fSpread,
float fAccuracyFishtail,
float flSoundTime,
WeaponSound_t sound_type,
float flRecoilIndex
)
{
bool bDoEffects = true;
if ( fInaccuracy > 1.0f )
fInaccuracy = 1.0f;
#ifdef CLIENT_DLL
C_CSPlayer *pPlayer = ToCSPlayer( ClientEntityList().GetBaseEntity( iPlayerIndex ) );
#else
CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( iPlayerIndex) );
#endif
if ( !pPlayer || iPlayerIndex < 0 )
{
// probably an env_gunfire
const CCSWeaponInfo* pWeaponInfo = GetWeaponInfo( iWeaponID );
FX_WeaponSound( iPlayerIndex, nItemDefIndex, sound_type, vOrigin, pWeaponInfo, flSoundTime, PITCH_NORM );
#ifndef CLIENT_DLL
// if this is server code, send the effect over to client as temp entity
// Dispatch one message for all the bullet impacts and sounds.
TE_FireBullets(
-1,
nItemDefIndex,
vOrigin,
vAngles,
iWeaponID,
iMode,
iSeed,
fInaccuracy,
fSpread,
fAccuracyFishtail,
sound_type,
flRecoilIndex
);
#endif
return;
}
#ifdef CLIENT_DLL
CWeaponCSBase* pClientWeapon = pPlayer ? pPlayer->GetActiveCSWeapon() : NULL;
if ( pClientWeapon )
{
if ( gpGlobals->curtime - pClientWeapon->m_flLastClientFireBulletTime > 0.02f ) // this should be enough even for the negev, at ~1000 rof
{
pClientWeapon->m_flLastClientFireBulletTime = gpGlobals->curtime;
}
else
{
return; // we already traced this shot on the client!
}
}
#endif
QAngle adjustedAngles = vAngles;
adjustedAngles.y += fAccuracyFishtail;
if ( pPlayer && debug_aim_angle.GetBool() )
{
QAngle old = pPlayer->EyeAngles() + pPlayer->GetAimPunchAngle();
#ifdef CLIENT_DLL
DevMsg("Client ");
#else
DevMsg("Server ");
#endif
DevMsg("old: %f %f new: %f %f\n",
old[YAW], old[PITCH],
vAngles[YAW], vAngles[PITCH]
);
if ( debug_aim_angle.GetInt() == 2 )
{
adjustedAngles = old;
}
}
const CEconItemDefinition* pItemDef = GetItemSchema()->GetItemDefinition( nItemDefIndex );
if ( !pItemDef )
{
DevMsg( "FX_FireBullets: GetItemDefinition failed for defindex %d\n", nItemDefIndex );
return;
}
#if !defined(CLIENT_DLL)
if ( weapon_accuracy_logging.GetBool() )
{
char szFlags[256];
V_strcpy(szFlags, " ");
// #if defined(CLIENT_DLL)
// V_strcat(szFlags, "CLIENT ", sizeof(szFlags));
// #else
// V_strcat(szFlags, "SERVER ", sizeof(szFlags));
// #endif
//
if ( pPlayer->GetMoveType() == MOVETYPE_LADDER )
V_strcat(szFlags, "LADDER ", sizeof(szFlags));
if ( FBitSet( pPlayer->GetFlags(), FL_ONGROUND ) )
V_strcat(szFlags, "GROUND ", sizeof(szFlags));
if ( FBitSet( pPlayer->GetFlags(), FL_DUCKING) )
V_strcat(szFlags, "DUCKING ", sizeof(szFlags));
float fVelocity = pPlayer->GetAbsVelocity().Length2D();
Msg("FireBullets @ %10f [ %s ]: inaccuracy=%f spread=%f max dispersion=%f mode=%2i vel=%10f seed=%3i %s\n",
gpGlobals->curtime, pItemDef->GetItemBaseName(), fInaccuracy, fSpread, fInaccuracy + fSpread, iMode, fVelocity, iSeed, szFlags);
}
#endif
WEAPON_FILE_INFO_HANDLE hWpnInfo = LookupWeaponInfoSlot( pItemDef->GetItemClass() );
if ( hWpnInfo == GetInvalidWeaponInfoHandle() )
{
DevMsg("FX_FireBullets: LookupWeaponInfoSlot failed for weapon %s\n", pItemDef->GetItemBaseName() );
return;
}
CCSWeaponInfo *pWeaponInfo = static_cast< CCSWeaponInfo* >( GetFileWeaponInfoFromHandle( hWpnInfo ) );
if ( !pWeaponInfo )
{
DevMsg( "FX_FireBullets: GetFileWeaponInfoFromHandle failed for weapon %s\n", pItemDef->GetItemBaseName() );
return;
}
// Do the firing animation event.
#ifndef CLIENT_DLL
if ( pPlayer && !pPlayer->IsDormant() )
{
if ( iMode == Primary_Mode )
pPlayer->DoAnimationEvent( PLAYERANIMEVENT_FIRE_GUN_PRIMARY );
else
pPlayer->DoAnimationEvent( PLAYERANIMEVENT_FIRE_GUN_SECONDARY );
}
#endif // CLIENT_DLL
#ifdef CLIENT_DLL
if ( pPlayer && pPlayer->m_bUseNewAnimstate )
{
pPlayer->ProcessMuzzleFlashEvent();
}
#endif
#ifndef CLIENT_DLL
// if this is server code, send the effect over to client as temp entity
// Dispatch one message for all the bullet impacts and sounds.
TE_FireBullets(
iPlayerIndex,
nItemDefIndex,
vOrigin,
vAngles,
iWeaponID,
iMode,
iSeed,
fInaccuracy,
fSpread,
fAccuracyFishtail,
sound_type,
flRecoilIndex
);
// Let the player remember the usercmd he fired a weapon on. Assists in making decisions about lag compensation.
if ( pPlayer )
pPlayer->NoteWeaponFired();
bDoEffects = false; // no effects on server
#endif
iSeed++;
CWeaponCSBase* pWeapon = pPlayer ? pPlayer->GetActiveCSWeapon() : NULL;
CEconItemView* pItem = pWeapon ? pWeapon->GetEconItemView() : NULL;
int iDamage = pWeaponInfo->GetDamage( pItem );
float flRange = pWeaponInfo->GetRange( pItem );
float flPenetration = pWeaponInfo->GetPenetration( pItem );
float flRangeModifier = pWeaponInfo->GetRangeModifier( pItem );
int iAmmoType = pWeaponInfo->GetPrimaryAmmoType( pItem );
if ( bDoEffects)
{
static const float MaxPitchShiftInaccuracy = 0.05f;
float flPitchShift = pWeaponInfo->GetInaccuracyPitchShift() * (fInaccuracy < MaxPitchShiftInaccuracy ? fInaccuracy : MaxPitchShiftInaccuracy);
if ( sound_type == SINGLE && pWeaponInfo->GetInaccuracyAltSoundThreshhold() > 0.0f && fInaccuracy < pWeaponInfo->GetInaccuracyAltSoundThreshhold() )
{
sound_type = SINGLE_ACCURATE;
flPitchShift = 0.0f;
}
FX_WeaponSound( iPlayerIndex, nItemDefIndex, sound_type, vOrigin, pWeaponInfo, flSoundTime, PITCH_NORM + int(flPitchShift) );
// If the gun's nearly empty, also play a subtle "nearly-empty" sound, since the weapon
// is lighter and acoustically different when weighed down by fewer bullets.
// But really it's so you get a fun low ammo warning from an audio cue.
if ( weapon_near_empty_sound.GetBool() &&
pWeapon && pWeapon->GetMaxClip1() > 1 && // not a single-shot weapon
(((float)pWeapon->m_iClip1) / ((float)pWeapon->GetMaxClip1()) <= 0.2) ) // 20% or fewer bullets remaining
{
FX_WeaponSound( iPlayerIndex, nItemDefIndex, NEARLYEMPTY, vOrigin, pWeaponInfo, flSoundTime, PITCH_NORM );
}
}
// Fire bullets, calculate impacts & effects
if ( !pPlayer )
return;
StartGroupingSounds();
#ifdef GAME_DLL
pPlayer->StartNewBulletGroup();
#endif
#if !defined (CLIENT_DLL)
// Move other players back to history positions based on local player's lag
lagcompensation->StartLagCompensation( pPlayer, LAG_COMPENSATE_HITBOXES_ALONG_RAY, vOrigin, vAngles, flRange );
#endif
// [sbodenbender] rumble when shooting
// since we are handling bullet fx in CS differently than other titles, call
// rumble effect directly instead of Player::RumbleEffect
//=============================================================================
#if defined (CLIENT_DLL)
if (pPlayer && pPlayer->IsLocalPlayer() && pWeaponInfo && pWeaponInfo->GetBullets() > 0)
{
int rumbleEffect = pWeaponInfo->iRumbleEffect;
if( rumbleEffect != RUMBLE_INVALID )
{
RumbleEffect( XBX_GetUserId( pPlayer->GetSplitScreenPlayerSlot() ), rumbleEffect, 0, RUMBLE_FLAG_RESTART );
}
if ( rumbleEffect != RUMBLE_INVALID && rumbleEffect <= 6 && steam_controller_haptics.GetBool() && g_pInputSystem->IsSteamControllerActive() && steamapicontext->SteamController() )
{
ControllerHandle_t handles[MAX_STEAM_CONTROLLERS];
int nControllers = steamapicontext->SteamController()->GetConnectedControllers( handles );
for ( int i = 0; i < nControllers; ++i )
{
steamapicontext->SteamController()->TriggerHapticPulse( handles[ i ], k_ESteamControllerPad_Right, (2000*rumbleEffect)/5 );
steamapicontext->SteamController()->TriggerHapticPulse( handles[ i ], k_ESteamControllerPad_Left, (2000*rumbleEffect)/5 );
}
}
}
#endif
bool bForceMaxInaccuracy = weapon_debug_max_inaccuracy.GetBool();
bool bForceInaccuracyDirection = weapon_debug_inaccuracy_only_up.GetBool();
RandomSeed( iSeed ); // init random system with this seed
// Accuracy curve density adjustment FOR R8 REVOLVER SECONDARY FIRE, NEGEV WILD BEAST
float flRadiusCurveDensity = RandomFloat();
if ( nItemDefIndex == 64 && iMode == Secondary_Mode ) /*R8 REVOLVER SECONDARY FIRE*/
{
flRadiusCurveDensity = 1.0f - flRadiusCurveDensity*flRadiusCurveDensity;
}
if ( nItemDefIndex == 28 && flRecoilIndex < 3 ) /*NEGEV WILD BEAST*/
{
for ( int j = 3; j > flRecoilIndex; -- j )
{
flRadiusCurveDensity *= flRadiusCurveDensity;
}
flRadiusCurveDensity = 1.0f - flRadiusCurveDensity;
}
if ( bForceMaxInaccuracy )
flRadiusCurveDensity = 1.0f;
// Get accuracy displacement
float fTheta0 = RandomFloat(0.0f, 2.0f * M_PI);
if ( bForceInaccuracyDirection )
fTheta0 = M_PI * 0.5f;
float fRadius0 = flRadiusCurveDensity * fInaccuracy;
float x0 = fRadius0 * cosf(fTheta0);
float y0 = fRadius0 * sinf(fTheta0);
const int kMaxBullets = 16;
float x1[kMaxBullets], y1[kMaxBullets];
Assert(pWeaponInfo->GetBullets() <= kMaxBullets);
// the RNG can be desynchronized by FireBullet(), so pre-generate all spread offsets
for ( int iBullet=0; iBullet < pWeaponInfo->GetBullets(); iBullet++ )
{
// Spread curve density adjustment for R8 REVOLVER SECONDARY FIRE, NEGEV WILD BEAST
float flSpreadCurveDensity = RandomFloat();
if ( nItemDefIndex == 64 && iMode == Secondary_Mode )
{
flSpreadCurveDensity = 1.0f - flSpreadCurveDensity*flSpreadCurveDensity;
}
if ( nItemDefIndex == 28 && flRecoilIndex < 3 ) /*NEGEV WILD BEAST*/
{
for ( int j = 3; j > flRecoilIndex; --j )
{
flSpreadCurveDensity *= flSpreadCurveDensity;
}
flSpreadCurveDensity = 1.0f - flSpreadCurveDensity;
}
if ( bForceMaxInaccuracy )
flSpreadCurveDensity = 1.0f;
float fTheta1 = RandomFloat(0.0f, 2.0f * M_PI);
if ( bForceInaccuracyDirection )
fTheta1 = M_PI * 0.5f;
float fRadius1 = flSpreadCurveDensity * fSpread;
x1[iBullet] = fRadius1 * cosf(fTheta1);
y1[iBullet] = fRadius1 * sinf(fTheta1);
}
#if !defined( CLIENT_DLL )
{ /// Make sure take damage listener stays in scope only for the duration of FireBullet loop below!
class CFireBulletTakeDamageListener : public CCSPlayer::ITakeDamageListener
{
public:
CFireBulletTakeDamageListener( CCSPlayer *pPlayerShooting ) :
m_pPlayerShooting(pPlayerShooting),
m_bEnemyHit( false ),
m_bShotFiredAndOnTargetRecorded( false )
{}
virtual void OnTakeDamageListenerCallback( CCSPlayer *pVictim, CTakeDamageInfo &infoTweakable ) OVERRIDE
{
if ( m_pPlayerShooting && pVictim->IsOtherEnemy( m_pPlayerShooting ) )
{
m_bEnemyHit = true;
if ( infoTweakable.GetDamageType() & DMG_HEADSHOT )
{
m_rbHsPlayers.InsertIfNotFound( pVictim ); // remember that at least one pellet hit a headshot
}
else if ( m_rbHsPlayers.Find( pVictim ) != m_rbHsPlayers.InvalidIndex() )
{
#if 0
DevMsg( "DMG: Pellet modified for headshot visualization %s -> %s = (0x%08X +hs)\n",
m_pPlayerShooting ? m_pPlayerShooting->GetPlayerName() : "[unknown]",
pVictim->GetPlayerName(), infoTweakable.GetDamageType() );
#endif
infoTweakable.SetDamageType( infoTweakable.GetDamageType() | DMG_HEADSHOT ); // since previous pellets hit a headshot we visualize it as a headshot
}
// Since we know that bullet was fired and that we hit the target
// we should record the accuracy stats right now, otherwise we may TerminateRound
// based on a kill from this bullet and not have this data recorded
RecordShotFiredAndOnTargetData();
}
}
void BulletBurstCompleted()
{
RecordShotFiredAndOnTargetData();
}
private:
void RecordShotFiredAndOnTargetData()
{
if ( m_bShotFiredAndOnTargetRecorded )
return;
m_bShotFiredAndOnTargetRecorded = true;
if ( m_pPlayerShooting && CSGameRules() && !CSGameRules()->IsWarmupPeriod() && !m_pPlayerShooting->IsBot() )
{
// Track in QMM total number of shots that connected with an opponent
if ( CCSGameRules::CQMMPlayerData_t *pQMM = CSGameRules()->QueuedMatchmakingPlayersDataFind( m_pPlayerShooting->GetHumanPlayerAccountID() ) )
{
++pQMM->m_numShotsFiredTotal;
if ( m_bEnemyHit )
++pQMM->m_numShotsOnTargetTotal;
}
}
}
private:
CCSPlayer *m_pPlayerShooting;
bool m_bEnemyHit;
bool m_bShotFiredAndOnTargetRecorded;
CUtlRBTree< CCSPlayer *, int, CDefLess< CCSPlayer * > > m_rbHsPlayers; // players who were dinked in the head as part of this bullet batch
} fbtdl( pPlayer );
#endif
for ( int iBullet=0; iBullet < pWeaponInfo->GetBullets(); iBullet++ )
{
if ( !pPlayer )
break;
int nPenetrationCount = 4;
pPlayer->FireBullet(
vOrigin,
adjustedAngles,
flRange,
flPenetration,
nPenetrationCount,
iAmmoType,
iDamage,
flRangeModifier,
pPlayer,
bDoEffects,
x0 + x1[iBullet], y0 + y1[iBullet]
);
}
#if !defined( CLIENT_DLL )
fbtdl.BulletBurstCompleted();
} /// Closes the lifetime scope of take damage listener in scope only for the duration of FireBullet loop above.
#endif
#if !defined (CLIENT_DLL)
lagcompensation->FinishLagCompensation( pPlayer );
#endif
EndGroupingSounds();
}
// This runs on both the client and the server.
// On the server, it dispatches a TE_PlantBomb to visible clients.
// On the client, it plays the planting animation.
void FX_PlantBomb( int iPlayerIndex, const Vector &vOrigin, PlantBombOption_t option )
{
#ifdef CLIENT_DLL
C_CSPlayer *pPlayer = ToCSPlayer( ClientEntityList().GetBaseEntity( iPlayerIndex ) );
#else
CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( iPlayerIndex) );
#endif
// Do the firing animation event.
if ( pPlayer && !pPlayer->IsDormant() )
{
switch ( option )
{
case PLANTBOMB_PLANT:
{
pPlayer->DoAnimStateEvent( PLAYERANIMEVENT_FIRE_GUN_PRIMARY );
}
break;
case PLANTBOMB_ABORT:
{
pPlayer->DoAnimStateEvent( PLAYERANIMEVENT_CLEAR_FIRING );
}
break;
}
}
#ifndef CLIENT_DLL
// if this is server code, send the effect over to client as temp entity
// Dispatch one message for all the bullet impacts and sounds.
TE_PlantBomb( iPlayerIndex, vOrigin, option );
#endif
}