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.
471 lines
14 KiB
471 lines
14 KiB
//========= Copyright © 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; |
|
|
|
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 |
|
{ |
|
if ( numExFound >= EXARRAYMAX ) |
|
continue; // SAFETY: don't overflow the array |
|
|
|
responseToSay[numExFound] = prospectiveResponse; |
|
pBestEx[numExFound] = pEx; |
|
bestScore = fpmax( score, bestScore ); |
|
numExFound += 1; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// 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 ) |
|
{ |
|
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" ); |
|
|
|
|