sqwarmed/sdk_src/game/server/ai_expresserfollowup.cpp

464 lines
16 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 "ai_speech.h"
#include "game.h"
#include "eventqueue.h"
#include "ai_basenpc.h"
#include "basemultiplayerplayer.h"
#include "ai_baseactor.h"
//#include "flex_expresser.h"
#include "scenefilecache/iscenefilecache.h"
/*
#include "engine/ienginesound.h"
#include "keyvalues.h"
#include "ai_criteria.h"
#include "isaverestore.h"
#include "sceneentity.h"
*/
// memdbgon must be the last include file in a .cpp file!!!
#include <tier0/memdbgon.h>
static const char *GetResponseName( CBaseEntity *pEnt )
{
Assert( pEnt );
if ( pEnt == NULL )
return "";
return STRING( pEnt->GetEntityName() );
}
// This is a tiny helper function for below -- what I'd use a lambda for, usually
static void DispatchComeback( CAI_ExpresserWithFollowup *pExpress, CBaseEntity *pSpeaker, CBaseEntity *pRespondent, AI_ResponseFollowup &followup )
{
AssertMsg(pSpeaker != NULL, "Response expressor somehow got called with a NULL Outer.\n");
if ( !pRespondent )
{
return;
}
float delay = followup.followup_delay;
if (pSpeaker == pRespondent && delay < 0)
{
Warning("Response rule with a 'self' target specified negative delay, which isn't legal because that would make someone talk over himself.");
delay = 0;
}
// Msg( "%s: Dispatch comeback about %s to %s\n", pSpeaker->GetBotString(), g_pConceptManager->GetTopicName( handle ), pRespondent->GetBotString() );
// build an input event that we will use to force the bot to talk through the IO system
variant_t value;
// Don't send along null contexts
if (followup.followup_contexts && followup.followup_contexts[0] != '\0')
{
value.SetString( MAKE_STRING( followup.followup_contexts ) );
g_EventQueue.AddEvent( pRespondent, "AddContext", value, delay - 0.01, pSpeaker, pSpeaker );
}
/*
value.SetString(MAKE_STRING(followup.followup_concept));
g_EventQueue.AddEvent( pRespondent, "SpeakResponseConcept", value, delay , pSpeaker, pSpeaker );
*/
AI_CriteriaSet criteria;
// add in the FROM context so dispatchee knows was from me
const char * RESTRICT pszSpeakerName = GetResponseName( pSpeaker );
criteria.AppendCriteria( "From", pszSpeakerName );
// if a SUBJECT criteria is missing, put it back in.
if ( criteria.FindCriterionIndex( "Subject" ) == -1 )
{
criteria.AppendCriteria( "Subject", pszSpeakerName );
}
// add in any provided contexts from the parameters onto the ones stored in the followup
criteria.Merge( followup.followup_contexts );
// This is kludgy and needs to be fixed in class hierarchy, but for now, try to guess at the most likely
// kinds of targets and dispatch to them.
if (CBaseMultiplayerPlayer *pPlayer = dynamic_cast<CBaseMultiplayerPlayer *>(pRespondent))
{
pPlayer->Speak( followup.followup_concept, &criteria );
}
else if (CAI_BaseActor *pActor = dynamic_cast<CAI_BaseActor *>(pRespondent))
{
pActor->Speak( followup.followup_concept, &criteria );
}
}
#if 0
//-----------------------------------------------------------------------------
// Purpose: Placeholder for rules based response system
// Input : concept -
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CAI_ExpresserWithFollowup::Speak( AIConcept_t &concept, const char *modifiers /*= NULL*/, char *pszOutResponseChosen /* = NULL*/, size_t bufsize /* = 0 */, IRecipientFilter *filter /* = NULL */ )
{
AI_Response *result = SpeakFindResponse( concept, modifiers );
if ( !result )
{
return false;
}
CNPC_CompanionBot *pBot = dynamic_cast<CNPC_CompanionBot *>(GetOuter());
if ( pBot )
{
pBot->SetConversationTopic( g_pConceptManager->GetTopic( handle ) );
pBot->SetLastSpeaker( g_pConceptManager->GetSpeaker( handle ) );
// Msg( "%s: Conversing about %s\n", pBot->GetBotString(), g_pConceptManager->GetTopicName( handle ) );
}
SpeechMsg( GetOuter(), "%s (%x) spoke %s (%f)\n", STRING(GetOuter()->GetEntityName()), GetOuter(), g_pConceptManager->GetConcept( handle ), gpGlobals->curtime );
bool spoke = SpeakDispatchResponse( handle, result, filter );
if ( pszOutResponseChosen )
{
result->GetResponse( pszOutResponseChosen, bufsize );
}
return spoke;
}
#endif
// Work out the character from the "subject" context.
// Right now, this is a simple find by entity name search.
// But you can define arbitrary subject names, like L4D does
// for "biker", "manager", etc.
static CBaseEntity *AscertainSpeechSubjectFromContext( AI_Response *response, AI_CriteriaSet &criteria, const char *pContextName )
{
const char *subject = criteria.GetValue( criteria.FindCriterionIndex( pContextName ) );
if (subject)
{
return gEntList.FindEntityByName( NULL, subject );
}
else
{
return NULL;
}
}
// TODO: Currently uses awful stricmp. Use symbols! Once I know which ones we want, that is.
static CResponseQueue::CFollowupTargetSpec_t ResolveFollowupTargetToEntity( AIConcept_t &concept, AI_CriteriaSet &criteria, const char * RESTRICT szTarget, AI_Response * RESTRICT response = NULL )
{
if ( Q_stricmp(szTarget, "self") == 0 )
{
return CResponseQueue::CFollowupTargetSpec_t( kDRT_SPECIFIC, concept.GetSpeaker() );
}
else if ( Q_stricmp(szTarget, "subject") == 0 )
{
return CResponseQueue::CFollowupTargetSpec_t( AscertainSpeechSubjectFromContext( response, criteria, "Subject" ) );
}
else if ( Q_stricmp(szTarget, "from") == 0 )
{
return CResponseQueue::CFollowupTargetSpec_t( AscertainSpeechSubjectFromContext( response, criteria, "From" ) );
}
else if ( Q_stricmp(szTarget, "any") == 0 )
{
return CResponseQueue::CFollowupTargetSpec_t( kDRT_ANY, concept.GetSpeaker() );
}
else if ( Q_stricmp(szTarget, "all") == 0 )
{
return CResponseQueue::CFollowupTargetSpec_t( kDRT_ALL );
}
// last resort, try a named lookup
else if ( CBaseEntity *pSpecific = gEntList.FindEntityByName(NULL, szTarget) ) // it could be anything
{
return CResponseQueue::CFollowupTargetSpec_t( pSpecific );
}
Warning("Couldn't resolve response target %s\n", szTarget );
return CResponseQueue::CFollowupTargetSpec_t(); // couldn't resolve.
}
// TODO: Currently uses awful stricmp. Use symbols! Once I know which ones we want, that is.
static CResponseQueue::CFollowupTargetSpec_t ResolveFollowupTargetToEntity( AIConcept_t &concept, AI_CriteriaSet &criteria, AI_Response * RESTRICT response, AI_ResponseFollowup * RESTRICT followup )
{
const char * RESTRICT szTarget = followup->followup_target;
const CResponseQueue::CFollowupTargetSpec_t INVALID; // default: invalid result
if ( szTarget == NULL )
return INVALID;
else
return ResolveFollowupTargetToEntity( concept, criteria, szTarget, response );
}
ConVar chet_debug_idle( "chet_debug_idle", "0", FCVAR_ARCHIVE, "If set one, many debug prints to help track down the TLK_IDLE issue. Set two for super verbose info" );
// extern ConVar chet_debug_idle;
bool CAI_ExpresserWithFollowup::Speak( AIConcept_t &concept, const char *modifiers /*= NULL*/, char *pszOutResponseChosen /* = NULL*/, size_t bufsize /* = 0 */, IRecipientFilter *filter /* = NULL */ )
{
VPROF("CAI_Expresser::Speak");
if ( IsSpeechGloballySuppressed() )
{
return false;
}
concept.SetSpeaker(GetOuter());
AI_CriteriaSet criteria;
GatherCriteria(&criteria, concept, modifiers);
GetOuter()->ModifyOrAppendDerivedCriteria(criteria);
AI_Response result;
if ( !FindResponse( result, concept, &criteria ) )
{
if (chet_debug_idle.GetBool())
{
const char *name = GetOuter()->GetDebugName();
Msg( "TLK_IDLE: %s did not FindResponse\n", name );
}
return false;
}
else
{
if (chet_debug_idle.GetBool())
{
const char *name = GetOuter()->GetDebugName();
Msg( "TLK_IDLE: %s SUCCESSFUL FindResponse\n", name );
}
}
SpeechMsg( GetOuter(), "%s (%x) spoke %s (%f)", STRING(GetOuter()->GetEntityName()), GetOuter(), (const char*)concept, gpGlobals->curtime );
// Msg( "%s:%s to %s:%s\n", GetOuter()->GetDebugName(), concept.GetStringConcept(), criteria.GetValue(criteria.FindCriterionIndex("Subject")), pTarget ? pTarget->GetDebugName() : "none" );
bool spoke = SpeakDispatchResponse( concept, &result, &criteria, filter );
if ( pszOutResponseChosen )
{
result.GetResponse( pszOutResponseChosen, bufsize );
}
return spoke;
}
extern ISceneFileCache *scenefilecache;
static float GetSpeechDurationForResponse( const AI_Response * RESTRICT response )
{
char sceneName[256];
response->GetResponse( sceneName, sizeof(sceneName) );
// look up the scene in the scene cache
SceneCachedData_t scenedata;
bool bHasData = scenefilecache->GetSceneCachedData( sceneName, &scenedata );
AssertMsg1( bHasData, "Couldn't find %s in scene cache.", sceneName );
if ( bHasData )
{
return scenedata.m_fLastSpeakSecs;
}
else
{
return -1;
}
}
//-----------------------------------------------------------------------------
// Purpose: Dispatches the result
// Input : *response -
//-----------------------------------------------------------------------------
bool CAI_ExpresserWithFollowup::SpeakDispatchResponse( AIConcept_t &concept, AI_Response *response, AI_CriteriaSet *criteria, IRecipientFilter *filter )
{
// This gives the chance for the other bot to respond.
if ( !concept.GetSpeaker().IsValid() )
{
concept.SetSpeaker(GetOuter());
}
bool bInterrupted = IsSpeaking();
bool bSuc = CAI_Expresser::SpeakDispatchResponse( concept, response, criteria, filter );
if (!bSuc)
{
return false;
}
if ( bInterrupted )
{
g_ResponseQueueManager.GetQueue()->RemoveSpeechQueuedFor( GetOuter() );
}
// Record my followup details so that I may defer its use til end of the speech
AI_ResponseFollowup * RESTRICT followup = response->GetParams()->m_pFollowup;
if ( followup )
{
if ( followup->followup_entityiotarget && followup->followup_entityioinput )
if ( criteria )
{
CBaseEntity * RESTRICT pTarget = gEntList.FindEntityByName( NULL, followup->followup_entityiotarget );
if ( pTarget )
{
g_EventQueue.AddEvent( pTarget, followup->followup_entityioinput, variant_t(), followup->followup_entityiodelay, GetOuter(), GetOuter() );
}
}
if ( followup->IsValid() )
{
// 11th hour change: rather than trigger followups from the end of a VCD,
// instead fire it from the end of the last speech event in the VCD, because
// there's a multisecond facial relax delay built into the scene.
// The speech length is stored in the cache, so we can post the followup now.
if ( response->GetType() == ResponseRules::RESPONSE_SCENE &&
followup->followup_delay >= 0 )
{
float fTimeToLastSpeech = GetSpeechDurationForResponse( response );
// failsafe
if ( fTimeToLastSpeech > 0 )
{
DispatchFollowupThroughQueue( followup->followup_concept, followup->followup_contexts,
ResolveFollowupTargetToEntity( concept, *criteria, response, followup ),
fTimeToLastSpeech + followup->followup_delay, GetOuter() );
}
else // error
{
// old way, copied from "else" below
m_pPostponedFollowup = followup;
if ( criteria )
m_followupTarget = ResolveFollowupTargetToEntity( concept, *criteria, response, m_pPostponedFollowup );
else
{
AI_CriteriaSet tmpCriteria;
m_followupTarget = ResolveFollowupTargetToEntity( concept, tmpCriteria, response, m_pPostponedFollowup );
}
}
}
else if ( followup->followup_delay < 0 )
{
// a negative delay has a special meaning. Usually the comeback dispatches after
// the currently said line is finished; the delay is added to that, to provide a
// pause between when character A finishes speaking and B begins.
// A negative delay (-n) actually means "dispatch the comeback n seconds
// after I start talking".
// In this case we do not need to postpone the followup; we just throw it directly
// into the queue.
DispatchFollowupThroughQueue( followup->followup_concept, followup->followup_contexts,
ResolveFollowupTargetToEntity( concept, *criteria, response, followup ),
-followup->followup_delay, GetOuter() );
}
else if ( response->GetType() == ResponseRules::RESPONSE_PRINT )
{ // zero-duration responses dispatch immediately via the queue (must be the queue bec.
// the m_pPostponedFollowup will never trigger)
DispatchFollowupThroughQueue( followup->followup_concept, followup->followup_contexts,
ResolveFollowupTargetToEntity( concept, *criteria, response, followup ),
followup->followup_delay, GetOuter() );
}
else
{
// this is kind of a quick patch to immediately deal with the issue of null criteria
// (arose while branching to main) without replumbing a bunch of stuff -- to be fixed
// 5.13.08 egr
m_pPostponedFollowup = followup;
if ( criteria )
m_followupTarget = ResolveFollowupTargetToEntity( concept, *criteria, response, m_pPostponedFollowup );
else
{
AI_CriteriaSet tmpCriteria;
m_followupTarget = ResolveFollowupTargetToEntity( concept, tmpCriteria, response, m_pPostponedFollowup );
}
}
}
}
return bSuc;
}
// This is a gimmick used when a negative delay is specified in a followup, which is a shorthand
// for "this many seconds after the beginning of the line" rather than "this may seconds after the end
// of the line", eg to create a THEN rule when two characters talk over each other.
// It's static to avoid accidental use of the postponed followup/target members.
void CAI_ExpresserWithFollowup::DispatchFollowupThroughQueue( const AIConcept_t &concept,
const char * RESTRICT criteriaStr,
const CResponseQueue::CFollowupTargetSpec_t &target,
float delay,
CBaseEntity * RESTRICT pOuter
)
{
AI_CriteriaSet criteria;
// Don't add my own criteria! GatherCriteria( &criteria, followup.followup_concept, followup.followup_contexts );
criteria.AppendCriteria( "From", STRING( pOuter->GetEntityName() ) );
criteria.Merge( criteriaStr );
g_ResponseQueueManager.GetQueue()->Add( concept, &criteria, gpGlobals->curtime + delay, target, pOuter );
}
//-----------------------------------------------------------------------------
// Purpose: Handles the new concept objects
//-----------------------------------------------------------------------------
void CAI_ExpresserWithFollowup::SpeakDispatchFollowup( AI_ResponseFollowup &followup )
{
if ( !m_followupTarget.IsValid() )
return;
// If a specific entity target is given, use the old pathway for now
if ( m_followupTarget.m_iTargetType == kDRT_SPECIFIC && followup.followup_delay == 0 )
{
CBaseEntity *pTarget = m_followupTarget.m_hHandle.Get();
if (!pTarget)
{
return;
}
DispatchComeback( this, GetOuter(), pTarget, followup );
}
else
{
DispatchFollowupThroughQueue( followup.followup_concept, followup.followup_contexts, m_followupTarget, followup.followup_delay, GetOuter() );
}
// clear out the followup member just in case.
m_pPostponedFollowup = NULL;
m_followupTarget.m_iTargetType = kDRT_MAX;
}
void CAI_ExpresserWithFollowup::OnSpeechFinished()
{
if (m_pPostponedFollowup && m_pPostponedFollowup->IsValid())
{
return SpeakDispatchFollowup(*m_pPostponedFollowup);
}
}
void CC_RR_ForceConcept_f( const CCommand &args )
{
if ( args.ArgC() < 3 )
{
Msg("USAGE: rr_forceconcept <target> <concept> \"criteria1:value1,criteria2:value2,...\"\n");
return;
}
AI_CriteriaSet criteria;
if ( args.ArgC() >= 3 )
{
const char *criteriastring = args[3];
criteria.Merge( criteriastring );
}
AIConcept_t concept( args[2] );
QueueSpeak( concept, ResolveFollowupTargetToEntity( concept, criteria, args[1] ), criteria );
}
static ConCommand rr_forceconcept( "rr_forceconcept", CC_RR_ForceConcept_f,
"fire a response concept directly at a given character.\n"
"USAGE: rr_forceconcept <target> <concept> \"criteria1:value1,criteria2:value2,...\"\n"
"criteria values are optional.\n"
, FCVAR_CHEAT );