source-engine/game/server/soundent.cpp

809 lines
22 KiB
C++
Raw Normal View History

2020-04-22 16:56:21 +00:00
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $NoKeywords: $
//
//=============================================================================//
#include "cbase.h"
#include "soundent.h"
#include "game.h"
#include "world.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//-----------------------------------------------------------------------------
// Some enumerations needed by CSoundEnt
//-----------------------------------------------------------------------------
// identifiers passed to functions that can operate on either list, to indicate which list to operate on.
#define SOUNDLISTTYPE_FREE 1
#define SOUNDLISTTYPE_ACTIVE 2
LINK_ENTITY_TO_CLASS( soundent, CSoundEnt );
static CSoundEnt *g_pSoundEnt = NULL;
BEGIN_SIMPLE_DATADESC( CSound )
DEFINE_FIELD( m_hOwner, FIELD_EHANDLE ),
DEFINE_FIELD( m_iVolume, FIELD_INTEGER ),
DEFINE_FIELD( m_flOcclusionScale, FIELD_FLOAT ),
DEFINE_FIELD( m_iType, FIELD_INTEGER ),
// DEFINE_FIELD( m_iNextAudible, FIELD_INTEGER ),
DEFINE_FIELD( m_bNoExpirationTime, FIELD_BOOLEAN ),
DEFINE_FIELD( m_flExpireTime, FIELD_TIME ),
DEFINE_FIELD( m_iNext, FIELD_SHORT ),
DEFINE_FIELD( m_ownerChannelIndex, FIELD_INTEGER ),
DEFINE_FIELD( m_vecOrigin, FIELD_POSITION_VECTOR ),
DEFINE_FIELD( m_bHasOwner, FIELD_BOOLEAN ),
// DEFINE_FIELD( m_iMyIndex, FIELD_INTEGER ),
DEFINE_FIELD( m_hTarget, FIELD_EHANDLE ),
END_DATADESC()
//=========================================================
// CSound - Clear - zeros all fields for a sound
//=========================================================
void CSound::Clear ( void )
{
m_vecOrigin = vec3_origin;
m_iType = 0;
m_iVolume = 0;
m_flOcclusionScale = 0;
m_flExpireTime = 0;
m_bNoExpirationTime = false;
m_iNext = SOUNDLIST_EMPTY;
m_iNextAudible = 0;
}
//=========================================================
// Reset - clears the volume, origin, and type for a sound,
// but doesn't expire or unlink it.
//=========================================================
void CSound::Reset ( void )
{
m_vecOrigin = vec3_origin;
m_iType = 0;
m_iVolume = 0;
m_iNext = SOUNDLIST_EMPTY;
}
//=========================================================
// FIsSound - returns true if the sound is an Audible sound
//=========================================================
bool CSound::FIsSound ( void )
{
switch( SoundTypeNoContext() )
{
case SOUND_COMBAT:
case SOUND_WORLD:
case SOUND_PLAYER:
case SOUND_DANGER:
case SOUND_DANGER_SNIPERONLY:
case SOUND_THUMPER:
case SOUND_BULLET_IMPACT:
case SOUND_BUGBAIT:
case SOUND_PHYSICS_DANGER:
case SOUND_MOVE_AWAY:
case SOUND_PLAYER_VEHICLE:
return true;
default:
return false;
}
}
//=========================================================
// FIsScent - returns true if the sound is actually a scent
// do we really need this function? If a sound isn't a sound,
// it must be a scent. (sjb)
//=========================================================
bool CSound::FIsScent ( void )
{
switch( m_iType )
{
case SOUND_CARCASS:
case SOUND_MEAT:
case SOUND_GARBAGE:
return true;
default:
return false;
}
}
//---------------------------------------------------------
// This function returns the spot the listener should be
// interested in if he hears the sound. MOST of the time,
// this spot is the same as the sound's origin. But sometimes
// (like with bullet impacts) the entity that owns the
// sound is more interesting than the actual location of the
// sound effect.
//---------------------------------------------------------
const Vector &CSound::GetSoundReactOrigin( void )
{
// Check pure types.
switch( m_iType )
{
case SOUND_BULLET_IMPACT:
case SOUND_PHYSICS_DANGER:
if( m_hOwner.Get() != NULL )
{
// We really want the origin of this sound's
// owner.
return m_hOwner->GetAbsOrigin();
}
else
{
// If the owner is somehow invalid, we'll settle
// for the sound's origin rather than a crash.
return GetSoundOrigin();
}
break;
}
if( m_iType & SOUND_CONTEXT_REACT_TO_SOURCE )
{
if( m_hOwner.Get() != NULL )
{
return m_hOwner->GetAbsOrigin();
}
}
// Check for types with additional context.
if( m_iType & SOUND_DANGER )
{
if( (m_iType & SOUND_CONTEXT_FROM_SNIPER) )
{
if( m_hOwner.Get() != NULL )
{
// Be afraid of the sniper's location, not where the bullet will hit.
return m_hOwner->GetAbsOrigin();
}
else
{
return GetSoundOrigin();
}
}
}
return GetSoundOrigin();
}
//-----------------------------------------------------------------------------
// Save/load
//-----------------------------------------------------------------------------
BEGIN_DATADESC( CSoundEnt )
DEFINE_FIELD( m_iFreeSound, FIELD_INTEGER ),
DEFINE_FIELD( m_iActiveSound, FIELD_INTEGER ),
DEFINE_FIELD( m_cLastActiveSounds, FIELD_INTEGER ),
DEFINE_EMBEDDED_ARRAY( m_SoundPool, MAX_WORLD_SOUNDS_SP ),
END_DATADESC()
//-----------------------------------------------------------------------------
// Class factory methods
//-----------------------------------------------------------------------------
bool CSoundEnt::InitSoundEnt()
{
///!!!LATER - do we want a sound ent in deathmatch? (sjb)
g_pSoundEnt = (CSoundEnt*)CBaseEntity::Create( "soundent", vec3_origin, vec3_angle, GetWorldEntity() );
if ( !g_pSoundEnt )
{
Warning( "**COULD NOT CREATE SOUNDENT**\n" );
return false;
}
g_pSoundEnt->AddEFlags( EFL_KEEP_ON_RECREATE_ENTITIES );
return true;
}
void CSoundEnt::ShutdownSoundEnt()
{
if ( g_pSoundEnt )
{
g_pSoundEnt->FreeList();
g_pSoundEnt = NULL;
}
}
//-----------------------------------------------------------------------------
// Construction, destruction
//-----------------------------------------------------------------------------
CSoundEnt::CSoundEnt()
{
}
CSoundEnt::~CSoundEnt()
{
}
//=========================================================
// Spawn
//=========================================================
void CSoundEnt::Spawn( void )
{
SetSolid( SOLID_NONE );
Initialize();
SetNextThink( gpGlobals->curtime + 1 );
}
void CSoundEnt::OnRestore()
{
BaseClass::OnRestore();
// Make sure the singleton points to the restored version of this.
if ( g_pSoundEnt )
{
Assert( g_pSoundEnt != this );
UTIL_Remove( g_pSoundEnt );
}
g_pSoundEnt = this;
}
//=========================================================
// Think - at interval, the entire active sound list is checked
// for sounds that have ExpireTimes less than or equal
// to the current world time, and these sounds are deallocated.
//=========================================================
void CSoundEnt::Think ( void )
{
int iSound;
int iPreviousSound;
SetNextThink( gpGlobals->curtime + 0.1 );// how often to check the sound list.
iPreviousSound = SOUNDLIST_EMPTY;
iSound = m_iActiveSound;
while ( iSound != SOUNDLIST_EMPTY )
{
if ( (m_SoundPool[ iSound ].m_flExpireTime <= gpGlobals->curtime && (!m_SoundPool[ iSound ].m_bNoExpirationTime)) || !m_SoundPool[iSound].ValidateOwner() )
{
int iNext = m_SoundPool[ iSound ].m_iNext;
if( displaysoundlist.GetInt() == 1 )
{
Msg(" Removed Sound: %d (Time:%f)\n", m_SoundPool[ iSound ].SoundType(), gpGlobals->curtime );
}
if( displaysoundlist.GetInt() == 2 && m_SoundPool[ iSound ].IsSoundType( SOUND_DANGER ) )
{
Msg(" Removed Danger Sound: %d (time:%f)\n", m_SoundPool[ iSound ].SoundType(), gpGlobals->curtime );
}
// move this sound back into the free list
FreeSound( iSound, iPreviousSound );
iSound = iNext;
}
else
{
if( displaysoundlist.GetBool() )
{
Vector forward, right, up;
GetVectors( &forward, &right, &up );
byte r, g, b;
// Default to yellow.
r = 255;
g = 255;
b = 0;
CSound *pSound = &m_SoundPool[ iSound ];
if( pSound->IsSoundType( SOUND_DANGER ) )
{
r = 255;
g = 0;
b = 0;
}
if( displaysoundlist.GetInt() == 1 || (displaysoundlist.GetInt() == 2 && pSound->IsSoundType( SOUND_DANGER ) ) )
{
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + forward * pSound->Volume(), r,g,b, false, 0.1 );
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - forward * pSound->Volume(), r,g,b, false, 0.1 );
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + right * pSound->Volume(), r,g,b, false, 0.1 );
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - right * pSound->Volume(), r,g,b, false, 0.1 );
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + up * pSound->Volume(), r,g,b, false, 0.1 );
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - up * pSound->Volume(), r,g,b, false, 0.1 );
if( pSound->m_flOcclusionScale != 1.0 )
{
// Draw the occluded radius, too.
r = 0; g = 150; b = 255;
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + forward * pSound->OccludedVolume(), r,g,b, false, 0.1 );
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - forward * pSound->OccludedVolume(), r,g,b, false, 0.1 );
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + right * pSound->OccludedVolume(), r,g,b, false, 0.1 );
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - right * pSound->OccludedVolume(), r,g,b, false, 0.1 );
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() + up * pSound->OccludedVolume(), r,g,b, false, 0.1 );
NDebugOverlay::Line( pSound->GetSoundOrigin(), pSound->GetSoundOrigin() - up * pSound->OccludedVolume(), r,g,b, false, 0.1 );
}
}
DevMsg( 2, "Soundlist: %d / %d (%d)\n", ISoundsInList( SOUNDLISTTYPE_ACTIVE ),ISoundsInList( SOUNDLISTTYPE_FREE ), ISoundsInList( SOUNDLISTTYPE_ACTIVE ) - m_cLastActiveSounds );
m_cLastActiveSounds = ISoundsInList ( SOUNDLISTTYPE_ACTIVE );
}
iPreviousSound = iSound;
iSound = m_SoundPool[ iSound ].m_iNext;
}
}
}
//=========================================================
// Precache - dummy function
//=========================================================
void CSoundEnt::Precache ( void )
{
}
//=========================================================
// FreeSound - clears the passed active sound and moves it
// to the top of the free list. TAKE CARE to only call this
// function for sounds in the Active list!!
//=========================================================
void CSoundEnt::FreeSound ( int iSound, int iPrevious )
{
if ( !g_pSoundEnt )
{
// no sound ent!
return;
}
if ( iPrevious != SOUNDLIST_EMPTY )
{
// iSound is not the head of the active list, so
// must fix the index for the Previous sound
g_pSoundEnt->m_SoundPool[ iPrevious ].m_iNext = g_pSoundEnt->m_SoundPool[ iSound ].m_iNext;
}
else
{
// the sound we're freeing IS the head of the active list.
g_pSoundEnt->m_iActiveSound = g_pSoundEnt->m_SoundPool [ iSound ].m_iNext;
}
// make iSound the head of the Free list.
g_pSoundEnt->m_SoundPool[ iSound ].m_iNext = g_pSoundEnt->m_iFreeSound;
g_pSoundEnt->m_iFreeSound = iSound;
}
//=========================================================
// IAllocSound - moves a sound from the Free list to the
// Active list returns the index of the alloc'd sound
//=========================================================
int CSoundEnt::IAllocSound( void )
{
int iNewSound;
if ( m_iFreeSound == SOUNDLIST_EMPTY )
{
// no free sound!
if ( developer.GetInt() >= 2 )
Msg( "Free Sound List is full!\n" );
return SOUNDLIST_EMPTY;
}
// there is at least one sound available, so move it to the
// Active sound list, and return its SoundPool index.
iNewSound = m_iFreeSound;// copy the index of the next free sound
m_iFreeSound = m_SoundPool[ m_iFreeSound ].m_iNext;// move the index down into the free list.
m_SoundPool[ iNewSound ].m_iNext = m_iActiveSound;// point the new sound at the top of the active list.
m_iActiveSound = iNewSound;// now make the new sound the top of the active list. You're done.
#ifdef DEBUG
m_SoundPool[ iNewSound ].m_iMyIndex = iNewSound;
#endif // DEBUG
return iNewSound;
}
//=========================================================
// InsertSound - Allocates a free sound and fills it with
// sound info.
//=========================================================
void CSoundEnt::InsertSound ( int iType, const Vector &vecOrigin, int iVolume, float flDuration, CBaseEntity *pOwner, int soundChannelIndex, CBaseEntity *pSoundTarget )
{
int iThisSound;
if ( !g_pSoundEnt )
return;
if( soundChannelIndex == SOUNDENT_CHANNEL_UNSPECIFIED )
{
// No sound channel specified. So just make a new sound.
iThisSound = g_pSoundEnt->IAllocSound();
}
else
{
// If this entity has already got a sound in the soundlist that's on this
// channel, update that sound. Otherwise add a new one.
iThisSound = g_pSoundEnt->FindOrAllocateSound( pOwner, soundChannelIndex );
}
if ( iThisSound == SOUNDLIST_EMPTY )
{
DevMsg( "Could not AllocSound() for InsertSound() (Game DLL)\n" );
return;
}
CSound *pSound;
pSound = &g_pSoundEnt->m_SoundPool[ iThisSound ];
pSound->SetSoundOrigin( vecOrigin );
pSound->m_iType = iType;
pSound->m_iVolume = iVolume;
pSound->m_flOcclusionScale = 0.5;
pSound->m_flExpireTime = gpGlobals->curtime + flDuration;
pSound->m_bNoExpirationTime = false;
pSound->m_hOwner.Set( pOwner );
pSound->m_hTarget.Set( pSoundTarget );
pSound->m_ownerChannelIndex = soundChannelIndex;
// Keep track of whether this sound had an owner when it was made. If the sound has a long duration,
// the owner could disappear by the time someone hears this sound, so we have to look at this boolean
// and throw out sounds who have a NULL owner but this field set to true. (sjb) 12/2/2005
if( pOwner )
{
pSound->m_bHasOwner = true;
}
else
{
pSound->m_bHasOwner = false;
}
if( displaysoundlist.GetInt() == 1 )
{
Msg(" Added Sound! Type:%d Duration:%f (Time:%f)\n", pSound->SoundType(), flDuration, gpGlobals->curtime );
}
if( displaysoundlist.GetInt() == 2 && (iType & SOUND_DANGER) )
{
Msg(" Added Danger Sound! Duration:%f (Time:%f)\n", flDuration, gpGlobals->curtime );
}
}
//---------------------------------------------------------
//---------------------------------------------------------
int CSoundEnt::FindOrAllocateSound( CBaseEntity *pOwner, int soundChannelIndex )
{
int iSound = m_iActiveSound;
while ( iSound != SOUNDLIST_EMPTY )
{
CSound &sound = m_SoundPool[iSound];
if ( sound.m_ownerChannelIndex == soundChannelIndex && sound.m_hOwner == pOwner )
{
return iSound;
}
iSound = sound.m_iNext;
}
return IAllocSound();
}
//=========================================================
// Initialize - clears all sounds and moves them into the
// free sound list.
//=========================================================
void CSoundEnt::Initialize ( void )
{
int i;
int iSound;
m_cLastActiveSounds;
m_iFreeSound = 0;
m_iActiveSound = SOUNDLIST_EMPTY;
// In SP, we should only use the first 64 slots so save/load works right.
// In MP, have one for each player and 32 extras.
int nTotalSoundsInPool = MAX_WORLD_SOUNDS_SP;
if ( gpGlobals->maxClients > 1 )
nTotalSoundsInPool = MIN( MAX_WORLD_SOUNDS_MP, gpGlobals->maxClients + 32 );
if ( gpGlobals->maxClients+16 > nTotalSoundsInPool )
{
Warning( "CSoundEnt pool is low on sounds due to high number of clients.\n" );
}
for ( i = 0 ; i < nTotalSoundsInPool ; i++ )
{
// clear all sounds, and link them into the free sound list.
m_SoundPool[ i ].Clear();
m_SoundPool[ i ].m_iNext = i + 1;
}
m_SoundPool[ i - 1 ].m_iNext = SOUNDLIST_EMPTY;// terminate the list here.
// now reserve enough sounds for each client
for ( i = 0 ; i < gpGlobals->maxClients ; i++ )
{
iSound = IAllocSound();
if ( iSound == SOUNDLIST_EMPTY )
{
DevMsg( "Could not AllocSound() for Client Reserve! (DLL)\n" );
return;
}
m_SoundPool[ iSound ].m_bNoExpirationTime = true;
}
}
//=========================================================
// ISoundsInList - returns the number of sounds in the desired
// sound list.
//=========================================================
int CSoundEnt::ISoundsInList ( int iListType )
{
int i;
int iThisSound = SOUNDLIST_EMPTY;
if ( iListType == SOUNDLISTTYPE_FREE )
{
iThisSound = m_iFreeSound;
}
else if ( iListType == SOUNDLISTTYPE_ACTIVE )
{
iThisSound = m_iActiveSound;
}
else
{
Msg( "Unknown Sound List Type!\n" );
}
if ( iThisSound == SOUNDLIST_EMPTY )
{
return 0;
}
i = 0;
while ( iThisSound != SOUNDLIST_EMPTY )
{
i++;
iThisSound = m_SoundPool[ iThisSound ].m_iNext;
}
return i;
}
//=========================================================
// ActiveList - returns the head of the active sound list
//=========================================================
int CSoundEnt::ActiveList ( void )
{
if ( !g_pSoundEnt )
{
return SOUNDLIST_EMPTY;
}
return g_pSoundEnt->m_iActiveSound;
}
//=========================================================
// FreeList - returns the head of the free sound list
//=========================================================
int CSoundEnt::FreeList ( void )
{
if ( !g_pSoundEnt )
{
return SOUNDLIST_EMPTY;
}
return g_pSoundEnt->m_iFreeSound;
}
//=========================================================
// SoundPointerForIndex - returns a pointer to the instance
// of CSound at index's position in the sound pool.
//=========================================================
CSound* CSoundEnt::SoundPointerForIndex( int iIndex )
{
if ( !g_pSoundEnt )
{
return NULL;
}
if ( iIndex > ( MAX_WORLD_SOUNDS_MP - 1 ) )
{
Msg( "SoundPointerForIndex() - Index too large!\n" );
return NULL;
}
if ( iIndex < 0 )
{
Msg( "SoundPointerForIndex() - Index < 0!\n" );
return NULL;
}
return &g_pSoundEnt->m_SoundPool[ iIndex ];
}
//=========================================================
// Clients are numbered from 1 to MAXCLIENTS, but the client
// reserved sounds in the soundlist are from 0 to MAXCLIENTS - 1,
// so this function ensures that a client gets the proper index
// to his reserved sound in the soundlist.
//=========================================================
int CSoundEnt::ClientSoundIndex ( edict_t *pClient )
{
int iReturn = ENTINDEX( pClient ) - 1;
#ifdef _DEBUG
if ( iReturn < 0 || iReturn >= gpGlobals->maxClients )
{
Msg( "** ClientSoundIndex returning a bogus value! **\n" );
}
#endif // _DEBUG
return iReturn;
}
//-----------------------------------------------------------------------------
// Purpose: Return the loudest sound of the specified type at "earposition"
//-----------------------------------------------------------------------------
CSound* CSoundEnt::GetLoudestSoundOfType( int iType, const Vector &vecEarPosition )
{
CSound *pLoudestSound = NULL;
int iThisSound;
int iBestSound = SOUNDLIST_EMPTY;
float flBestDist = MAX_COORD_RANGE*MAX_COORD_RANGE;// so first nearby sound will become best so far.
float flDist;
CSound *pSound;
iThisSound = ActiveList();
while ( iThisSound != SOUNDLIST_EMPTY )
{
pSound = SoundPointerForIndex( iThisSound );
if ( pSound && pSound->m_iType == iType && pSound->ValidateOwner() )
{
flDist = ( pSound->GetSoundOrigin() - vecEarPosition ).Length();
//FIXME: This doesn't match what's in Listen()
//flDist = UTIL_DistApprox( pSound->GetSoundOrigin(), vecEarPosition );
if ( flDist <= pSound->m_iVolume && flDist < flBestDist )
{
pLoudestSound = pSound;
iBestSound = iThisSound;
flBestDist = flDist;
}
}
iThisSound = pSound->m_iNext;
}
return pLoudestSound;
}
//-----------------------------------------------------------------------------
// Purpose: Inserts an AI sound into the world sound list.
//-----------------------------------------------------------------------------
class CAISound : public CPointEntity
{
public:
CAISound()
{
// Initialize these new keyvalues appropriately
// in order to support legacy instances of ai_sound.
m_iSoundContext = 0x00000000;
m_iVolume = 0;
m_flDuration = 0.3;
}
DECLARE_CLASS( CAISound, CPointEntity );
DECLARE_DATADESC();
// data
int m_iSoundType;
int m_iSoundContext;
int m_iVolume;
float m_flDuration;
string_t m_iszProxyEntityName;
// Input handlers
void InputInsertSound( inputdata_t &inputdata );
void InputEmitAISound( inputdata_t &inputdata );
};
LINK_ENTITY_TO_CLASS( ai_sound, CAISound );
BEGIN_DATADESC( CAISound )
DEFINE_KEYFIELD( m_iSoundType, FIELD_INTEGER, "soundtype" ),
DEFINE_KEYFIELD( m_iSoundContext, FIELD_INTEGER, "soundcontext" ),
DEFINE_KEYFIELD( m_iVolume, FIELD_INTEGER, "volume" ),
DEFINE_KEYFIELD( m_flDuration, FIELD_FLOAT, "duration" ),
DEFINE_KEYFIELD( m_iszProxyEntityName, FIELD_STRING, "locationproxy" ),
DEFINE_INPUTFUNC( FIELD_INTEGER, "InsertSound", InputInsertSound ),
DEFINE_INPUTFUNC( FIELD_VOID, "EmitAISound", InputEmitAISound ),
END_DATADESC()
//-----------------------------------------------------------------------------
// Purpose: *** OBSOLETE **** Here for legacy support only!
//-----------------------------------------------------------------------------
void CAISound::InputInsertSound( inputdata_t &inputdata )
{
int iVolume;
iVolume = inputdata.value.Int();
Vector vecLocation = GetAbsOrigin();
if( m_iszProxyEntityName != NULL_STRING )
{
CBaseEntity *pProxy = gEntList.FindEntityByName( NULL, m_iszProxyEntityName );
if( pProxy )
{
vecLocation = pProxy->GetAbsOrigin();
}
else
{
DevWarning("Warning- ai_sound cannot find proxy entity named '%s'. Using self.\n", STRING(m_iszProxyEntityName) );
}
}
g_pSoundEnt->InsertSound( m_iSoundType, vecLocation, iVolume, m_flDuration, this );
}
void CAISound::InputEmitAISound( inputdata_t &inputdata )
{
Vector vecLocation = GetAbsOrigin();
if( m_iszProxyEntityName != NULL_STRING )
{
CBaseEntity *pProxy = gEntList.FindEntityByName( NULL, m_iszProxyEntityName );
if( pProxy )
{
vecLocation = pProxy->GetAbsOrigin();
}
else
{
DevWarning("Warning- ai_sound cannot find proxy entity named '%s'. Using self.\n", STRING(m_iszProxyEntityName) );
}
}
g_pSoundEnt->InsertSound( m_iSoundType | m_iSoundContext, vecLocation, m_iVolume, m_flDuration, this );
}