|
|
//========= 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
if ( response.IsQuashed() ) { idx = m_Queue.Next(idx); // is now the next index
// 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
}
idx = m_Queue.Next(idx); // is now the next index
// we can delete this entry now
m_Queue.Remove(previdx); } else { idx = m_Queue.Next(idx); // is now the next index
} } }
/// 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" );
|