//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //=============================================================================// #include "cbase.h" #include "weapon_csbase.h" #include "decals.h" #include "cs_gamerules.h" #include "weapon_c4.h" #include "in_buttons.h" #include "datacache/imdlcache.h" #ifdef CLIENT_DLL #include "c_cs_player.h" #else #include "cs_player.h" #include "soundent.h" #include "bot/cs_bot.h" #include "KeyValues.h" #include "triggers.h" #include "cs_gamestats.h" #endif #include "cs_playeranimstate.h" #include "basecombatweapon_shared.h" #include "util_shared.h" #include "takedamageinfo.h" #include "effect_dispatch_data.h" #include "engine/ivdebugoverlay.h" #include "obstacle_pushaway.h" #include "props_shared.h" ConVar sv_showimpacts("sv_showimpacts", "0", FCVAR_REPLICATED, "Shows client (red) and server (blue) bullet impact point (1=both, 2=client-only, 3=server-only)" ); ConVar sv_showplayerhitboxes( "sv_showplayerhitboxes", "0", FCVAR_REPLICATED, "Show lag compensated hitboxes for the specified player index whenever a player fires." ); #define CS_MASK_SHOOT (MASK_SOLID|CONTENTS_DEBRIS) void DispatchEffect( const char *pName, const CEffectData &data ); #ifdef _DEBUG // This is some extra code to collect weapon accuracy stats: struct bulletdata_s { float timedelta; // time delta since first shot of this round float derivation; // derivation for first shoot view angle int count; }; #define STATS_MAX_BULLETS 50 static bulletdata_s s_bullet_stats[STATS_MAX_BULLETS]; Vector s_firstImpact = Vector(0,0,0); float s_firstTime = 0; float s_LastTime = 0; int s_bulletCount = 0; void ResetBulletStats() { s_firstTime = 0; s_LastTime = 0; s_bulletCount = 0; s_firstImpact = Vector(0,0,0); Q_memset( s_bullet_stats, 0, sizeof(s_bullet_stats) ); } void PrintBulletStats() { for (int i=0; i s_LastTime + 2.0f ) { // time delta since last shoot is bigger than 2 seconds, start new row s_LastTime = s_firstTime = time; s_bulletCount = 0; s_firstImpact = impact; } else { s_LastTime = time; s_bulletCount++; } if ( s_bulletCount >= STATS_MAX_BULLETS ) s_bulletCount = STATS_MAX_BULLETS -1; if ( dist < 1 ) dist = 1; int i = s_bulletCount; float offset = VectorLength( s_firstImpact - impact ); float timedelta = time - s_firstTime; float derivation = offset / dist; float weight = (float)s_bullet_stats[i].count/(float)(s_bullet_stats[i].count+1); s_bullet_stats[i].timedelta *= weight; s_bullet_stats[i].timedelta += (1.0f-weight) * timedelta; s_bullet_stats[i].derivation *= weight; s_bullet_stats[i].derivation += (1.0f-weight) * derivation; s_bullet_stats[i].count++; } CON_COMMAND( stats_bullets_reset, "Reset bullet stats") { ResetBulletStats(); } CON_COMMAND( stats_bullets_print, "Print bullet stats") { PrintBulletStats(); } #endif float CCSPlayer::GetPlayerMaxSpeed() { if ( GetMoveType() == MOVETYPE_NONE ) { return CS_PLAYER_SPEED_STOPPED; } if ( IsObserver() ) { // Player gets speed bonus in observer mode return CS_PLAYER_SPEED_OBSERVER; } bool bValidMoveState = ( State_Get() == STATE_ACTIVE || State_Get() == STATE_OBSERVER_MODE ); if ( !bValidMoveState || m_bIsDefusing || CSGameRules()->IsFreezePeriod() ) { // Player should not move during the freeze period return CS_PLAYER_SPEED_STOPPED; } float speed = BaseClass::GetPlayerMaxSpeed(); if ( IsVIP() == true ) // VIP is slow due to the armour he's wearing { speed = MIN(speed, CS_PLAYER_SPEED_VIP); } else { CWeaponCSBase *pWeapon = dynamic_cast( GetActiveWeapon() ); if ( pWeapon ) { if ( HasShield() && IsShieldDrawn() ) { speed = MIN(speed, CS_PLAYER_SPEED_SHIELD); } else { speed = MIN(speed, pWeapon->GetMaxSpeed()); } } } return speed; } void CCSPlayer::GetBulletTypeParameters( int iBulletType, float &fPenetrationPower, float &flPenetrationDistance ) { //MIKETODO: make ammo types come from a script file. if ( IsAmmoType( iBulletType, BULLET_PLAYER_50AE ) ) { fPenetrationPower = 30; flPenetrationDistance = 1000.0; } else if ( IsAmmoType( iBulletType, BULLET_PLAYER_762MM ) ) { fPenetrationPower = 39; flPenetrationDistance = 5000.0; } else if ( IsAmmoType( iBulletType, BULLET_PLAYER_556MM ) || IsAmmoType( iBulletType, BULLET_PLAYER_556MM_BOX ) ) { fPenetrationPower = 35; flPenetrationDistance = 4000.0; } else if ( IsAmmoType( iBulletType, BULLET_PLAYER_338MAG ) ) { fPenetrationPower = 45; flPenetrationDistance = 8000.0; } else if ( IsAmmoType( iBulletType, BULLET_PLAYER_9MM ) ) { fPenetrationPower = 21; flPenetrationDistance = 800.0; } else if ( IsAmmoType( iBulletType, BULLET_PLAYER_BUCKSHOT ) ) { fPenetrationPower = 0; flPenetrationDistance = 0.0; } else if ( IsAmmoType( iBulletType, BULLET_PLAYER_45ACP ) ) { fPenetrationPower = 15; flPenetrationDistance = 500.0; } else if ( IsAmmoType( iBulletType, BULLET_PLAYER_357SIG ) ) { fPenetrationPower = 25; flPenetrationDistance = 800.0; } else if ( IsAmmoType( iBulletType, BULLET_PLAYER_57MM ) ) { fPenetrationPower = 30; flPenetrationDistance = 2000.0; } else { // What kind of ammo is this? Assert( false ); fPenetrationPower = 0; flPenetrationDistance = 0.0; } } static void GetMaterialParameters( int iMaterial, float &flPenetrationModifier, float &flDamageModifier ) { switch ( iMaterial ) { case CHAR_TEX_METAL : flPenetrationModifier = 0.5; // If we hit metal, reduce the thickness of the brush we can't penetrate flDamageModifier = 0.3; break; case CHAR_TEX_DIRT : flPenetrationModifier = 0.5; flDamageModifier = 0.3; break; case CHAR_TEX_CONCRETE : flPenetrationModifier = 0.4; flDamageModifier = 0.25; break; case CHAR_TEX_GRATE : flPenetrationModifier = 1.0; flDamageModifier = 0.99; break; case CHAR_TEX_VENT : flPenetrationModifier = 0.5; flDamageModifier = 0.45; break; case CHAR_TEX_TILE : flPenetrationModifier = 0.65; flDamageModifier = 0.3; break; case CHAR_TEX_COMPUTER : flPenetrationModifier = 0.4; flDamageModifier = 0.45; break; case CHAR_TEX_WOOD : flPenetrationModifier = 1.0; flDamageModifier = 0.6; break; default : flPenetrationModifier = 1.0; flDamageModifier = 0.5; break; } Assert( flPenetrationModifier > 0 ); Assert( flDamageModifier < 1.0f ); // Less than 1.0f for avoiding infinite loops } static bool TraceToExit(Vector &start, Vector &dir, Vector &end, float flStepSize, float flMaxDistance ) { float flDistance = 0; Vector last = start; while ( flDistance <= flMaxDistance ) { flDistance += flStepSize; end = start + flDistance *dir; if ( (UTIL_PointContents ( end ) & MASK_SOLID) == 0 ) { // found first free point return true; } } return false; } inline void UTIL_TraceLineIgnoreTwoEntities( const Vector& vecAbsStart, const Vector& vecAbsEnd, unsigned int mask, const IHandleEntity *ignore, const IHandleEntity *ignore2, int collisionGroup, trace_t *ptr ) { Ray_t ray; ray.Init( vecAbsStart, vecAbsEnd ); CTraceFilterSkipTwoEntities traceFilter( ignore, ignore2, collisionGroup ); enginetrace->TraceRay( ray, mask, &traceFilter, ptr ); if( r_visualizetraces.GetBool() ) { DebugDrawLine( ptr->startpos, ptr->endpos, 255, 0, 0, true, -1.0f ); } } void CCSPlayer::FireBullet( Vector vecSrc, // shooting postion const QAngle &shootAngles, //shooting angle float flDistance, // max distance int iPenetration, // how many obstacles can be penetrated int iBulletType, // ammo type int iDamage, // base damage float flRangeModifier, // damage range modifier CBaseEntity *pevAttacker, // shooter bool bDoEffects, float xSpread, float ySpread ) { float fCurrentDamage = iDamage; // damage of the bullet at it's current trajectory float flCurrentDistance = 0.0; //distance that the bullet has traveled so far Vector vecDirShooting, vecRight, vecUp; AngleVectors( shootAngles, &vecDirShooting, &vecRight, &vecUp ); // MIKETODO: put all the ammo parameters into a script file and allow for CS-specific params. float flPenetrationPower = 0; // thickness of a wall that this bullet can penetrate float flPenetrationDistance = 0; // distance at which the bullet is capable of penetrating a wall float flDamageModifier = 0.5; // default modification of bullets power after they go through a wall. float flPenetrationModifier = 1.f; GetBulletTypeParameters( iBulletType, flPenetrationPower, flPenetrationDistance ); if ( !pevAttacker ) pevAttacker = this; // the default attacker is ourselves // add the spray Vector vecDir = vecDirShooting + xSpread * vecRight + ySpread * vecUp; VectorNormalize( vecDir ); //Adrian: visualize server/client player positions //This is used to show where the lag compesator thinks the player should be at. #if 0 for ( int k = 1; k <= gpGlobals->maxClients; k++ ) { CBasePlayer *clientClass = (CBasePlayer *)CBaseEntity::Instance( k ); if ( clientClass == NULL ) continue; if ( k == entindex() ) continue; #ifdef CLIENT_DLL debugoverlay->AddBoxOverlay( clientClass->GetAbsOrigin(), clientClass->WorldAlignMins(), clientClass->WorldAlignMaxs(), QAngle( 0, 0, 0), 255,0,0,127, 4 ); #else NDebugOverlay::Box( clientClass->GetAbsOrigin(), clientClass->WorldAlignMins(), clientClass->WorldAlignMaxs(), 0,0,255,127, 4 ); #endif } #endif //============================================================================= // HPE_BEGIN: //============================================================================= #ifndef CLIENT_DLL // [pfreese] Track number player entities killed with this bullet int iPenetrationKills = 0; // [menglish] Increment the shots fired for this player CCS_GameStats.Event_ShotFired( this, GetActiveWeapon() ); #endif //============================================================================= // HPE_END //============================================================================= bool bFirstHit = true; CBasePlayer *lastPlayerHit = NULL; if( sv_showplayerhitboxes.GetInt() > 0 ) { CBasePlayer *lagPlayer = UTIL_PlayerByIndex( sv_showplayerhitboxes.GetInt() ); if( lagPlayer ) { #ifdef CLIENT_DLL lagPlayer->DrawClientHitboxes(4, true); #else lagPlayer->DrawServerHitboxes(4, true); #endif } } MDLCACHE_CRITICAL_SECTION(); while ( fCurrentDamage > 0 ) { Vector vecEnd = vecSrc + vecDir * flDistance; trace_t tr; // main enter bullet trace UTIL_TraceLineIgnoreTwoEntities( vecSrc, vecEnd, CS_MASK_SHOOT|CONTENTS_HITBOX, this, lastPlayerHit, COLLISION_GROUP_NONE, &tr ); { CTraceFilterSkipTwoEntities filter( this, lastPlayerHit, COLLISION_GROUP_NONE ); // Check for player hitboxes extending outside their collision bounds const float rayExtension = 40.0f; UTIL_ClipTraceToPlayers( vecSrc, vecEnd + vecDir * rayExtension, CS_MASK_SHOOT|CONTENTS_HITBOX, &filter, &tr ); } lastPlayerHit = ToBasePlayer(tr.m_pEnt); if ( tr.fraction == 1.0f ) break; // we didn't hit anything, stop tracing shoot #ifdef _DEBUG if ( bFirstHit ) AddBulletStat( gpGlobals->realtime, VectorLength( vecSrc-tr.endpos), tr.endpos ); #endif bFirstHit = false; #ifndef CLIENT_DLL // // Propogate a bullet impact event // @todo Add this for shotgun pellets (which dont go thru here) // IGameEvent * event = gameeventmanager->CreateEvent( "bullet_impact" ); if ( event ) { event->SetInt( "userid", GetUserID() ); event->SetFloat( "x", tr.endpos.x ); event->SetFloat( "y", tr.endpos.y ); event->SetFloat( "z", tr.endpos.z ); gameeventmanager->FireEvent( event ); } #endif /************* MATERIAL DETECTION ***********/ surfacedata_t *pSurfaceData = physprops->GetSurfaceData( tr.surface.surfaceProps ); int iEnterMaterial = pSurfaceData->game.material; GetMaterialParameters( iEnterMaterial, flPenetrationModifier, flDamageModifier ); bool hitGrate = tr.contents & CONTENTS_GRATE; // since some railings in de_inferno are CONTENTS_GRATE but CHAR_TEX_CONCRETE, we'll trust the // CONTENTS_GRATE and use a high damage modifier. if ( hitGrate ) { // If we're a concrete grate (TOOLS/TOOLSINVISIBLE texture) allow more penetrating power. flPenetrationModifier = 1.0f; flDamageModifier = 0.99f; } #ifdef CLIENT_DLL if ( sv_showimpacts.GetInt() == 1 || sv_showimpacts.GetInt() == 2 ) { // draw red client impact markers debugoverlay->AddBoxOverlay( tr.endpos, Vector(-2,-2,-2), Vector(2,2,2), QAngle( 0, 0, 0), 255,0,0,127, 4 ); if ( tr.m_pEnt && tr.m_pEnt->IsPlayer() ) { C_BasePlayer *player = ToBasePlayer( tr.m_pEnt ); player->DrawClientHitboxes( 4, true ); } } #else if ( sv_showimpacts.GetInt() == 1 || sv_showimpacts.GetInt() == 3 ) { // draw blue server impact markers NDebugOverlay::Box( tr.endpos, Vector(-2,-2,-2), Vector(2,2,2), 0,0,255,127, 4 ); if ( tr.m_pEnt && tr.m_pEnt->IsPlayer() ) { CBasePlayer *player = ToBasePlayer( tr.m_pEnt ); player->DrawServerHitboxes( 4, true ); } } #endif //calculate the damage based on the distance the bullet travelled. flCurrentDistance += tr.fraction * flDistance; fCurrentDamage *= pow (flRangeModifier, (flCurrentDistance / 500)); // check if we reach penetration distance, no more penetrations after that if (flCurrentDistance > flPenetrationDistance && iPenetration > 0) iPenetration = 0; #ifndef CLIENT_DLL // This just keeps track of sounds for AIs (it doesn't play anything). CSoundEnt::InsertSound( SOUND_BULLET_IMPACT, tr.endpos, 400, 0.2f, this ); #endif int iDamageType = DMG_BULLET | DMG_NEVERGIB; if( bDoEffects ) { // See if the bullet ended up underwater + started out of the water if ( enginetrace->GetPointContents( tr.endpos ) & (CONTENTS_WATER|CONTENTS_SLIME) ) { trace_t waterTrace; UTIL_TraceLine( vecSrc, tr.endpos, (MASK_SHOT|CONTENTS_WATER|CONTENTS_SLIME), this, COLLISION_GROUP_NONE, &waterTrace ); if( waterTrace.allsolid != 1 ) { CEffectData data; data.m_vOrigin = waterTrace.endpos; data.m_vNormal = waterTrace.plane.normal; data.m_flScale = random->RandomFloat( 8, 12 ); if ( waterTrace.contents & CONTENTS_SLIME ) { data.m_fFlags |= FX_WATER_IN_SLIME; } DispatchEffect( "gunshotsplash", data ); } } else { //Do Regular hit effects // Don't decal nodraw surfaces if ( !( tr.surface.flags & (SURF_SKY|SURF_NODRAW|SURF_HINT|SURF_SKIP) ) ) { CBaseEntity *pEntity = tr.m_pEnt; if ( !( !friendlyfire.GetBool() && pEntity && pEntity->GetTeamNumber() == GetTeamNumber() ) ) { UTIL_ImpactTrace( &tr, iDamageType ); } } } } // bDoEffects // add damage to entity that we hit #ifndef CLIENT_DLL ClearMultiDamage(); //============================================================================= // HPE_BEGIN: // [pfreese] Check if enemy players were killed by this bullet, and if so, // add them to the iPenetrationKills count //============================================================================= CBaseEntity *pEntity = tr.m_pEnt; CTakeDamageInfo info( pevAttacker, pevAttacker, fCurrentDamage, iDamageType ); CalculateBulletDamageForce( &info, iBulletType, vecDir, tr.endpos ); pEntity->DispatchTraceAttack( info, vecDir, &tr ); bool bWasAlive = pEntity->IsAlive(); TraceAttackToTriggers( info, tr.startpos, tr.endpos, vecDir ); ApplyMultiDamage(); if (bWasAlive && !pEntity->IsAlive() && pEntity->IsPlayer() && pEntity->GetTeamNumber() != GetTeamNumber()) { ++iPenetrationKills; } //============================================================================= // HPE_END //============================================================================= #endif // check if bullet can penetrate another entity if ( iPenetration == 0 && !hitGrate ) break; // no, stop // If we hit a grate with iPenetration == 0, stop on the next thing we hit if ( iPenetration < 0 ) break; Vector penetrationEnd; // try to penetrate object, maximum penetration is 128 inch if ( !TraceToExit( tr.endpos, vecDir, penetrationEnd, 24, 128 ) ) break; // find exact penetration exit trace_t exitTr; UTIL_TraceLine( penetrationEnd, tr.endpos, CS_MASK_SHOOT|CONTENTS_HITBOX, NULL, &exitTr ); if( exitTr.m_pEnt != tr.m_pEnt && exitTr.m_pEnt != NULL ) { // something was blocking, trace again UTIL_TraceLine( penetrationEnd, tr.endpos, CS_MASK_SHOOT|CONTENTS_HITBOX, exitTr.m_pEnt, COLLISION_GROUP_NONE, &exitTr ); } // get material at exit point pSurfaceData = physprops->GetSurfaceData( exitTr.surface.surfaceProps ); int iExitMaterial = pSurfaceData->game.material; hitGrate = hitGrate && ( exitTr.contents & CONTENTS_GRATE ); // if enter & exit point is wood or metal we assume this is // a hollow crate or barrel and give a penetration bonus if ( iEnterMaterial == iExitMaterial ) { if( iExitMaterial == CHAR_TEX_WOOD || iExitMaterial == CHAR_TEX_METAL ) { flPenetrationModifier *= 2; } } float flTraceDistance = VectorLength( exitTr.endpos - tr.endpos ); // check if bullet has enough power to penetrate this distance for this material if ( flTraceDistance > ( flPenetrationPower * flPenetrationModifier ) ) break; // bullet hasn't enough power to penetrate this distance // penetration was successful // bullet did penetrate object, exit Decal if ( bDoEffects ) { UTIL_ImpactTrace( &exitTr, iDamageType ); } //setup new start end parameters for successive trace flPenetrationPower -= flTraceDistance / flPenetrationModifier; flCurrentDistance += flTraceDistance; // NDebugOverlay::Box( exitTr.endpos, Vector(-2,-2,-2), Vector(2,2,2), 0,255,0,127, 8 ); vecSrc = exitTr.endpos; flDistance = (flDistance - flCurrentDistance) * 0.5; // reduce damage power each time we hit something other than a grate fCurrentDamage *= flDamageModifier; // reduce penetration counter iPenetration--; } #ifndef CLIENT_DLL //============================================================================= // HPE_BEGIN: // [pfreese] If we killed at least two enemies with a single bullet, award the // TWO_WITH_ONE_SHOT achievement //============================================================================= if (iPenetrationKills >= 2) { AwardAchievement(CSKillTwoWithOneShot); } //============================================================================= // HPE_END //============================================================================= #endif } void CCSPlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOrigin, const Vector &vecVelocity ) { float speedSqr = vecVelocity.AsVector2D().LengthSqr(); // the fastest walk is 135 ( scout ), see CCSGameMovement::CheckParameters() if ( speedSqr < 150.0 * 150.0 ) return; // player is not running, no footsteps BaseClass::UpdateStepSound( psurface, vecOrigin, vecVelocity ); } // GOOSEMAN : Kick the view.. void CCSPlayer::KickBack( float up_base, float lateral_base, float up_modifier, float lateral_modifier, float up_max, float lateral_max, int direction_change ) { float flKickUp; float flKickLateral; if (m_iShotsFired == 1) // This is the first round fired { flKickUp = up_base; flKickLateral = lateral_base; } else { flKickUp = up_base + m_iShotsFired*up_modifier; flKickLateral = lateral_base + m_iShotsFired*lateral_modifier; } QAngle angle = GetPunchAngle(); angle.x -= flKickUp; if ( angle.x < -1 * up_max ) angle.x = -1 * up_max; if ( m_iDirection == 1 ) { angle.y += flKickLateral; if (angle.y > lateral_max) angle.y = lateral_max; } else { angle.y -= flKickLateral; if ( angle.y < -1 * lateral_max ) angle.y = -1 * lateral_max; } if ( !SharedRandomInt( "KickBack", 0, direction_change ) ) m_iDirection = 1 - m_iDirection; SetPunchAngle( angle ); } bool CCSPlayer::CanMove() const { // When we're in intro camera mode, it's important to return false here // so our physics object doesn't fall out of the world. if ( GetMoveType() == MOVETYPE_NONE ) return false; if ( IsObserver() ) return true; // observers can move all the time bool bValidMoveState = (State_Get() == STATE_ACTIVE || State_Get() == STATE_OBSERVER_MODE); if ( m_bIsDefusing || !bValidMoveState || CSGameRules()->IsFreezePeriod() ) { return false; } else { // Can't move while planting C4. CC4 *pC4 = dynamic_cast< CC4* >( GetActiveWeapon() ); if ( pC4 && pC4->m_bStartedArming ) return false; return true; } } void CCSPlayer::OnJump( float fImpulse ) { CWeaponCSBase* pActiveWeapon = GetActiveCSWeapon(); if ( pActiveWeapon != NULL ) pActiveWeapon->OnJump(fImpulse); } void CCSPlayer::OnLand( float fVelocity ) { CWeaponCSBase* pActiveWeapon = GetActiveCSWeapon(); if ( pActiveWeapon != NULL ) pActiveWeapon->OnLand(fVelocity); } //------------------------------------------------------------------------------------------------------------------------------- /** * Track the last time we were on a ladder, along with the ladder's normal and where we * were grabbing it, so we don't reach behind us and grab it again as we are trying to * dismount. */ void CCSPlayer::SurpressLadderChecks( const Vector& pos, const Vector& normal ) { m_ladderSurpressionTimer.Start( 1.0f ); m_lastLadderPos = pos; m_lastLadderNormal = normal; } //------------------------------------------------------------------------------------------------------------------------------- /** * Prevent us from re-grabbing the same ladder we were just on: * - if the timer is elapsed, let us grab again * - if the normal is different, let us grab * - if the 2D pos is very different, let us grab, since it's probably a different ladder */ bool CCSPlayer::CanGrabLadder( const Vector& pos, const Vector& normal ) { if ( m_ladderSurpressionTimer.GetRemainingTime() <= 0.0f ) { return true; } const float MaxDist = 64.0f; if ( pos.AsVector2D().DistToSqr( m_lastLadderPos.AsVector2D() ) < MaxDist * MaxDist ) { return false; } if ( normal != m_lastLadderNormal ) { return true; } return false; } void CCSPlayer::SetAnimation( PLAYER_ANIM playerAnim ) { // In CS, its CPlayerAnimState object manages ALL the animation state. return; } CWeaponCSBase* CCSPlayer::CSAnim_GetActiveWeapon() { return GetActiveCSWeapon(); } bool CCSPlayer::CSAnim_CanMove() { return CanMove(); } //-------------------------------------------------------------------------------------------------------------- #define MATERIAL_NAME_LENGTH 16 #ifdef GAME_DLL class CFootstepControl : public CBaseTrigger { public: DECLARE_CLASS( CFootstepControl, CBaseTrigger ); DECLARE_DATADESC(); DECLARE_SERVERCLASS(); virtual int UpdateTransmitState( void ); virtual void Spawn( void ); CNetworkVar( string_t, m_source ); CNetworkVar( string_t, m_destination ); }; LINK_ENTITY_TO_CLASS( func_footstep_control, CFootstepControl ); BEGIN_DATADESC( CFootstepControl ) DEFINE_KEYFIELD( m_source, FIELD_STRING, "Source" ), DEFINE_KEYFIELD( m_destination, FIELD_STRING, "Destination" ), END_DATADESC() IMPLEMENT_SERVERCLASS_ST( CFootstepControl, DT_FootstepControl ) SendPropStringT( SENDINFO(m_source) ), SendPropStringT( SENDINFO(m_destination) ), END_SEND_TABLE() int CFootstepControl::UpdateTransmitState( void ) { return SetTransmitState( FL_EDICT_ALWAYS ); } void CFootstepControl::Spawn( void ) { InitTrigger(); } #else //-------------------------------------------------------------------------------------------------------------- class C_FootstepControl : public C_BaseEntity { public: DECLARE_CLASS( C_FootstepControl, C_BaseEntity ); DECLARE_CLIENTCLASS(); C_FootstepControl( void ); ~C_FootstepControl(); char m_source[MATERIAL_NAME_LENGTH]; char m_destination[MATERIAL_NAME_LENGTH]; }; IMPLEMENT_CLIENTCLASS_DT(C_FootstepControl, DT_FootstepControl, CFootstepControl) RecvPropString( RECVINFO(m_source) ), RecvPropString( RECVINFO(m_destination) ), END_RECV_TABLE() CUtlVector< C_FootstepControl * > s_footstepControllers; C_FootstepControl::C_FootstepControl( void ) { s_footstepControllers.AddToTail( this ); } C_FootstepControl::~C_FootstepControl() { s_footstepControllers.FindAndRemove( this ); } surfacedata_t * CCSPlayer::GetFootstepSurface( const Vector &origin, const char *surfaceName ) { for ( int i=0; im_source, surfaceName ) ) { if ( control->CollisionProp()->IsPointInBounds( origin ) ) { return physprops->GetSurfaceData( physprops->GetSurfaceIndex( control->m_destination ) ); } } } return physprops->GetSurfaceData( physprops->GetSurfaceIndex( surfaceName ) ); } #endif