sqwarmed/sdk_src/game/server/ai_speechqueue.cpp

472 lines
14 KiB
C++
Raw Normal View History

2024-08-29 19:18:30 -04: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;
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" );