2021-03-08 02:11:13 -06:00
//========= Copyright <20> 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $NoKeywords: $
//=============================================================================//
# include "cbase.h"
# include "basemultiplayerplayer.h"
# include "ai_baseactor.h"
# include "ai_speech.h"
//#include "flex_expresser.h"
// memdbgon must be the last include file in a .cpp file!!!
# include <tier0/memdbgon.h>
extern ConVar ai_debug_speech ;
# define DebuggingSpeech() ai_debug_speech.GetBool()
extern ConVar rr_debugresponses ;
ConVar rr_followup_maxdist ( " rr_followup_maxdist " , " 1800 " , FCVAR_CHEAT , " 'then ANY' or 'then ALL' response followups will be dispatched only to characters within this distance. " ) ;
///////////////////////////////////////////////////////////////////////////////
// RESPONSE QUEUE DATA STRUCTURE
///////////////////////////////////////////////////////////////////////////////
CResponseQueue : : CResponseQueue ( int queueSize ) : m_Queue ( queueSize ) , m_ExpresserTargets ( 8 , 8 )
{ } ;
/// Add a deferred response.
void CResponseQueue : : Add ( const AIConcept_t & concept , ///< concept to dispatch
const AI_CriteriaSet * RESTRICT contexts ,
float time , ///< when to dispatch it. You can specify a time of zero to mean "immediately."
const CFollowupTargetSpec_t & targetspec ,
CBaseEntity * pIssuer
)
{
// Add a response.
AssertMsg ( m_Queue . Count ( ) < AI_RESPONSE_QUEUE_SIZE , " AI Response queue overfilled. " ) ;
QueueType_t : : IndexLocalType_t idx = m_Queue . AddToTail ( ) ;
m_Queue [ idx ] . Init ( concept , contexts , time , targetspec , pIssuer ) ;
}
/// Remove a deferred response matching the concept and issuer.
void CResponseQueue : : Remove ( const AIConcept_t & concept , ///< concept to dispatch
CBaseEntity * const RESTRICT pIssuer ///< the entity issuing the response, if one exists.
) RESTRICT
{
// walk through the queue until we find a response matching the concept and issuer, then strike it.
QueueType_t : : IndexLocalType_t idx = m_Queue . Head ( ) ;
while ( idx ! = m_Queue . InvalidIndex ( ) )
{
CDeferredResponse & response = m_Queue [ idx ] ;
QueueType_t : : IndexLocalType_t previdx = idx ; // advance the index immediately because we may be deleting the "current" element
idx = m_Queue . Next ( idx ) ; // is now the next index
if ( CompareConcepts ( response . m_concept , concept ) & & // if concepts match and
( ! pIssuer | | ( response . m_hIssuer . Get ( ) = = pIssuer ) ) // issuer is null, or matches the one in the response
)
{
m_Queue . Remove ( previdx ) ;
}
}
}
void CResponseQueue : : RemoveSpeechQueuedFor ( const CBaseEntity * pSpeaker )
{
// walk through the queue until we find a response matching the speaker, then strike it.
// because responses are dispatched from inside a loop that is already walking through the
// queue, it's not safe to actually remove the elements. Instead, quash it by replacing it
// with a null event.
for ( QueueType_t : : IndexLocalType_t idx = m_Queue . Head ( ) ;
idx ! = m_Queue . InvalidIndex ( ) ;
idx = m_Queue . Next ( idx ) ) // is now the next index
{
CDeferredResponse & response = m_Queue [ idx ] ;
if ( response . m_Target . m_hHandle . Get ( ) = = pSpeaker )
{
response . Quash ( ) ;
}
}
}
// TODO: use a more compact representation.
void CResponseQueue : : DeferContextsFromCriteriaSet ( DeferredContexts_t & contextsOut , const AI_CriteriaSet * RESTRICT criteriaIn )
{
contextsOut . Reset ( ) ;
if ( criteriaIn )
{
contextsOut . Merge ( criteriaIn ) ;
}
}
void CResponseQueue : : PerFrameDispatch ( )
{
failsafe :
// Walk through the list, find any messages whose time has come, and dispatch them. Then remove them.
QueueType_t : : IndexLocalType_t idx = m_Queue . Head ( ) ;
while ( idx ! = m_Queue . InvalidIndex ( ) )
{
// do we need to dispatch this concept?
CDeferredResponse & response = m_Queue [ idx ] ;
QueueType_t : : IndexLocalType_t previdx = idx ; // advance the index immediately because we may be deleting the "current" element
idx = m_Queue . Next ( idx ) ; // is now the next index
if ( response . IsQuashed ( ) )
{
// we can delete this entry now
m_Queue . Remove ( previdx ) ;
}
else if ( response . m_fDispatchTime < = gpGlobals - > curtime )
{
// dispatch. we've had bugs where dispatches removed things from inside the queue;
// so, as a failsafe, if the queue length changes as a result, start over.
int oldLength = m_Queue . Count ( ) ;
DispatchOneResponse ( response ) ;
if ( m_Queue . Count ( ) < oldLength )
{
AssertMsg ( false , " Response queue length changed in non-reentrant way! FAILSAFE TRIGGERED " ) ;
goto failsafe ; // ick
}
// we can delete this entry now
m_Queue . Remove ( previdx ) ;
}
}
}
/// Add an expressor owner to this queue.
void CResponseQueue : : AddExpresserHost ( CBaseEntity * host )
{
EHANDLE ehost ( host ) ;
// see if it's in there already
if ( m_ExpresserTargets . HasElement ( ehost ) )
{
AssertMsg1 ( false , " Tried to add %s to response queue when it was already in there. " , host - > GetDebugName ( ) ) ;
}
else
{
// zip through the queue front to back, first see if there's any invalid handles to replace
int count = m_ExpresserTargets . Count ( ) ;
for ( int i = 0 ; i < count ; + + i )
{
if ( ! m_ExpresserTargets [ i ] . Get ( ) )
{
m_ExpresserTargets [ i ] = ehost ;
return ;
}
}
// if we're down here we didn't find one to replace, so append the host to the end.
m_ExpresserTargets . AddToTail ( ehost ) ;
}
}
/// Remove an expresser host from this queue.
void CResponseQueue : : RemoveExpresserHost ( CBaseEntity * host )
{
int idx = m_ExpresserTargets . Find ( host ) ;
if ( idx = = - 1 )
{
// AssertMsg1(false, "Tried to remove %s from response queue, but it's not in there to begin with!", host->GetDebugName() );
}
else
{
m_ExpresserTargets . FastRemove ( idx ) ;
}
}
/// Get the expresser for a base entity.
/// TODO: Kind of an ugly hack until I get the class hierarchy straightened out.
static CAI_Expresser * InferExpresserFromBaseEntity ( CBaseEntity * RESTRICT pEnt )
{
if ( CBaseMultiplayerPlayer * pPlayer = dynamic_cast < CBaseMultiplayerPlayer * > ( pEnt ) )
{
return pPlayer - > GetExpresser ( ) ;
}
else if ( CAI_BaseActor * pActor = dynamic_cast < CAI_BaseActor * > ( pEnt ) )
{
return pActor - > GetExpresser ( ) ;
}
/*
else if ( CFlexExpresser * pFlex = dynamic_cast < CFlexExpresser * > ( pEnt ) )
{
return pFlex - > GetExpresser ( ) ;
}
*/
else
{
return NULL ;
}
}
void CResponseQueue : : CDeferredResponse : : Quash ( )
{
m_Target = CFollowupTargetSpec_t ( ) ;
m_fDispatchTime = 0 ;
}
bool CResponseQueue : : DispatchOneResponse ( CDeferredResponse & response )
{
// find the target.
CBaseEntity * RESTRICT pTarget = NULL ;
AI_CriteriaSet & deferredCriteria = response . m_contexts ;
CAI_Expresser * RESTRICT pEx = NULL ;
CBaseEntity * RESTRICT pIssuer = response . m_hIssuer . Get ( ) ; // MAY BE NULL
float followupMaxDistSq ;
{
/*
CFlexExpresser * RESTRICT pOrator = CFlexExpresser : : AsFlexExpresser ( pIssuer ) ;
if ( pOrator )
{
// max dist is overridden. "0" means infinite distance (for orators only),
// anything else is a finite distance.
if ( pOrator - > m_flThenAnyMaxDist > 0 )
{
followupMaxDistSq = pOrator - > m_flThenAnyMaxDist * pOrator - > m_flThenAnyMaxDist ;
}
else
{
followupMaxDistSq = FLT_MAX ;
}
}
else
*/
{
followupMaxDistSq = rr_followup_maxdist . GetFloat ( ) ; // square of max audibility distance
followupMaxDistSq * = followupMaxDistSq ;
}
}
switch ( response . m_Target . m_iTargetType )
{
case kDRT_SPECIFIC :
{
pTarget = response . m_Target . m_hHandle . Get ( ) ;
}
break ;
case kDRT_ANY :
{
return DispatchOneResponse_ThenANY ( response , & deferredCriteria , pIssuer , followupMaxDistSq ) ;
}
break ;
case kDRT_ALL :
{
bool bSaidAnything = false ;
Vector issuerLocation ;
if ( pIssuer )
{
issuerLocation = pIssuer - > GetAbsOrigin ( ) ;
}
// find all characters
int numExprs = GetNumExpresserTargets ( ) ;
for ( int i = 0 ; i < numExprs ; + + i )
{
pTarget = GetExpresserHost ( i ) ;
float distIssuerToTargetSq = 0.0f ;
if ( pIssuer )
{
distIssuerToTargetSq = ( pTarget - > GetAbsOrigin ( ) - issuerLocation ) . LengthSqr ( ) ;
if ( distIssuerToTargetSq > followupMaxDistSq )
continue ; // too far
}
pEx = InferExpresserFromBaseEntity ( pTarget ) ;
if ( ! pEx | | pTarget = = pIssuer )
continue ;
AI_CriteriaSet characterCriteria ;
pEx - > GatherCriteria ( & characterCriteria , response . m_concept , NULL ) ;
characterCriteria . Merge ( & deferredCriteria ) ;
if ( pIssuer )
{
characterCriteria . AppendCriteria ( " dist_from_issuer " , UTIL_VarArgs ( " %f " , sqrt ( distIssuerToTargetSq ) ) ) ;
}
AI_Response prospectiveResponse ;
if ( pEx - > FindResponse ( prospectiveResponse , response . m_concept , & characterCriteria ) )
{
// dispatch it
bSaidAnything = pEx - > SpeakDispatchResponse ( response . m_concept , & prospectiveResponse , & deferredCriteria ) | | bSaidAnything ;
}
}
return bSaidAnything ;
}
break ;
default :
// WTF?
AssertMsg1 ( false , " Unknown deferred response type %d \n " , response . m_Target . m_iTargetType ) ;
return false ;
}
if ( ! pTarget )
return false ; // we're done right here.
// Get the expresser for the target.
pEx = InferExpresserFromBaseEntity ( pTarget ) ;
if ( ! pEx )
return false ;
AI_CriteriaSet characterCriteria ;
pEx - > GatherCriteria ( & characterCriteria , response . m_concept , NULL ) ;
characterCriteria . Merge ( & deferredCriteria ) ;
pEx - > Speak ( response . m_concept , & characterCriteria ) ;
return true ;
}
//
ConVar rr_thenany_score_slop ( " rr_thenany_score_slop " , " 0.0 " , FCVAR_CHEAT , " When computing respondents for a 'THEN ANY' rule, all rule-matching scores within this much of the best score will be considered. " ) ;
# define EXARRAYMAX 32 // maximum number of prospective expressers in the array (hardcoded for simplicity)
bool CResponseQueue : : DispatchOneResponse_ThenANY ( CDeferredResponse & response , AI_CriteriaSet * RESTRICT pDeferredCriteria , CBaseEntity * const RESTRICT pIssuer , float followupMaxDistSq )
{
CBaseEntity * RESTRICT pTarget = NULL ;
CAI_Expresser * RESTRICT pEx = NULL ;
float bestScore = 0 ;
float slop = rr_thenany_score_slop . GetFloat ( ) ;
Vector issuerLocation ;
if ( pIssuer )
{
issuerLocation = pIssuer - > GetAbsOrigin ( ) ;
}
// this is an array of prospective respondents.
CAI_Expresser * RESTRICT pBestEx [ EXARRAYMAX ] ;
AI_Response responseToSay [ EXARRAYMAX ] ;
int numExFound = 0 ; // and this is the high water mark for the array.
// Here's the algorithm: we're going to walk through all the characters, finding the
// highest scoring ones for this rule. Let the highest score be called k.
// Because there may be (n) many characters all scoring k, we store an array of
// all characters with score k, then choose randomly from that array at return.
// We also define an allowable error for k in the global cvar
// rr_thenany_score_slop , which may be zero.
// find all characters (except the issuer)
int numExprs = GetNumExpresserTargets ( ) ;
AssertMsg1 ( numExprs < = EXARRAYMAX , " Response queue has %d possible expresser targets, please increase EXARRAYMAX " , numExprs ) ;
for ( int i = 0 ; i < numExprs ; + + i )
{
pTarget = GetExpresserHost ( i ) ;
if ( pTarget = = pIssuer )
continue ; // don't dispatch to myself
if ( ! pTarget - > IsAlive ( ) )
continue ; // dead men tell no tales
float distIssuerToTargetSq = 0.0f ;
if ( pIssuer )
{
distIssuerToTargetSq = ( pTarget - > GetAbsOrigin ( ) - issuerLocation ) . LengthSqr ( ) ;
if ( distIssuerToTargetSq > followupMaxDistSq )
continue ; // too far
}
pEx = InferExpresserFromBaseEntity ( pTarget ) ;
if ( ! pEx )
continue ;
AI_CriteriaSet characterCriteria ;
pEx - > GatherCriteria ( & characterCriteria , response . m_concept , NULL ) ;
characterCriteria . Merge ( pDeferredCriteria ) ;
pTarget - > ModifyOrAppendDerivedCriteria ( characterCriteria ) ;
if ( pIssuer )
{
characterCriteria . AppendCriteria ( " dist_from_issuer " , UTIL_VarArgs ( " %f " , sqrt ( distIssuerToTargetSq ) ) ) ;
}
AI_Response prospectiveResponse ;
2021-03-09 10:03:40 -06:00
# ifdef MAPBASE
pEx - > SetUsingProspectiveResponses ( true ) ;
# endif
2021-03-08 02:11:13 -06:00
if ( pEx - > FindResponse ( prospectiveResponse , response . m_concept , & characterCriteria ) )
{
float score = prospectiveResponse . GetMatchScore ( ) ;
if ( score > 0 & & ! prospectiveResponse . IsEmpty ( ) ) // ignore scores that are zero, regardless of slop
{
// if this score is better than all we've seen (outside the slop), then replace the array with
// an entry just to this expresser
if ( score > bestScore + slop )
{
responseToSay [ 0 ] = prospectiveResponse ;
pBestEx [ 0 ] = pEx ;
bestScore = score ;
numExFound = 1 ;
}
else if ( score > = bestScore - slop ) // if this score is at least as good as the best we've seen, but not better than all
{
2021-03-09 10:03:40 -06:00
if ( numExFound > = EXARRAYMAX )
{
# ifdef MAPBASE
pEx - > SetUsingProspectiveResponses ( false ) ;
# endif
2021-03-08 02:11:13 -06:00
continue ; // SAFETY: don't overflow the array
2021-03-09 10:03:40 -06:00
}
2021-03-08 02:11:13 -06:00
responseToSay [ numExFound ] = prospectiveResponse ;
pBestEx [ numExFound ] = pEx ;
bestScore = fpmax ( score , bestScore ) ;
numExFound + = 1 ;
}
}
}
2021-03-09 10:03:40 -06:00
# ifdef MAPBASE
pEx - > SetUsingProspectiveResponses ( false ) ;
# endif
2021-03-08 02:11:13 -06:00
}
// if I have a response, dispatch it.
if ( numExFound > 0 )
{
// get a random number between 0 and the responses found
int iSelect = numExFound > 1 ? RandomInt ( 0 , numExFound - 1 ) : 0 ;
if ( pBestEx [ iSelect ] ! = NULL )
{
2021-03-09 10:03:40 -06:00
# ifdef MAPBASE
pBestEx [ iSelect ] - > MarkResponseAsUsed ( responseToSay + iSelect ) ;
# endif
2021-03-08 02:11:13 -06:00
return pBestEx [ iSelect ] - > SpeakDispatchResponse ( response . m_concept , responseToSay + iSelect , pDeferredCriteria ) ;
}
else
{
AssertMsg ( false , " Response queue somehow found a response, but no expresser for it. \n " ) ;
return false ;
}
}
else
{ // I did not find a response.
return false ;
}
return false ; // just in case
}
void CResponseQueue : : Evacuate ( )
{
m_Queue . RemoveAll ( ) ;
}
# undef EXARRAYMAX
///////////////////////////////////////////////////////////////////////////////
// RESPONSE QUEUE MANAGER
///////////////////////////////////////////////////////////////////////////////
void CResponseQueueManager : : LevelInitPreEntity ( void )
{
if ( m_pQueue = = NULL )
{
m_pQueue = new CResponseQueue ( AI_RESPONSE_QUEUE_SIZE ) ;
}
}
CResponseQueueManager : : ~ CResponseQueueManager ( )
{
if ( m_pQueue ! = NULL )
{
delete m_pQueue ;
m_pQueue = NULL ;
}
}
void CResponseQueueManager : : Shutdown ( )
{
if ( m_pQueue ! = NULL )
{
delete m_pQueue ;
m_pQueue = NULL ;
}
}
void CResponseQueueManager : : FrameUpdatePostEntityThink ( )
{
Assert ( m_pQueue ) ;
m_pQueue - > PerFrameDispatch ( ) ;
}
CResponseQueueManager g_ResponseQueueManager ( " CResponseQueueManager " ) ;