//========= 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 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(pEnt) ) { return pPlayer->GetExpresser(); } else if ( CAI_BaseActor *pActor = dynamic_cast(pEnt) ) { return pActor->GetExpresser(); } else if ( CFlexExpresser *pFlex = dynamic_cast(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" );