Made followup responses more reliable with generic NPCs and added "vscript_file" response type

This commit is contained in:
Blixibon 2021-03-09 10:03:40 -06:00
parent d6b959899c
commit d4a91fe027
8 changed files with 235 additions and 6 deletions

View File

@ -74,6 +74,10 @@ static void DispatchComeback( CAI_ExpresserWithFollowup *pExpress, CBaseEntity *
// add in the FROM context so dispatchee knows was from me
const char * RESTRICT pszSpeakerName = GetResponseName( pSpeaker );
criteria.AppendCriteria( "From", pszSpeakerName );
#ifdef MAPBASE
// See DispatchFollowupThroughQueue()
criteria.AppendCriteria( "From_idx", CNumStr( pSpeaker->entindex() ) );
#endif
// if a SUBJECT criteria is missing, put it back in.
if ( criteria.FindCriterionIndex( "Subject" ) == -1 )
{
@ -140,8 +144,17 @@ static CBaseEntity *AscertainSpeechSubjectFromContext( AI_Response *response, AI
const char *subject = criteria.GetValue( criteria.FindCriterionIndex( pContextName ) );
if (subject)
{
CBaseEntity *pEnt = gEntList.FindEntityByName( NULL, subject );
return gEntList.FindEntityByName( NULL, subject );
#ifdef MAPBASE
// Allow entity indices to be used (see DispatchFollowupThroughQueue() for one particular use case)
if (!pEnt && atoi(subject))
{
pEnt = CBaseEntity::Instance( atoi( subject ) );
}
#endif
return pEnt;
}
else
@ -166,7 +179,12 @@ static CResponseQueue::CFollowupTargetSpec_t ResolveFollowupTargetToEntity( AICo
}
else if ( Q_stricmp(szTarget, "from") == 0 )
{
#ifdef MAPBASE
// See DispatchFollowupThroughQueue()
return CResponseQueue::CFollowupTargetSpec_t( AscertainSpeechSubjectFromContext( response, criteria, "From_idx" ) );
#else
return CResponseQueue::CFollowupTargetSpec_t( AscertainSpeechSubjectFromContext( response, criteria, "From" ) );
#endif
}
else if ( Q_stricmp(szTarget, "any") == 0 )
{
@ -400,6 +418,14 @@ void CAI_ExpresserWithFollowup::DispatchFollowupThroughQueue( const AIConcept_t
// Don't add my own criteria! GatherCriteria( &criteria, followup.followup_concept, followup.followup_contexts );
criteria.AppendCriteria( "From", STRING( pOuter->GetEntityName() ) );
#ifdef MAPBASE
// The index of the "From" entity.
// In HL2 mods, many followup users would be generic NPCs (e.g. citizens) who might not have any particular significance.
// Those generic NPCs are quite likely to have no name or have a name in common with other entities. As a result, Mapbase
// changes internal operations of the "From" context to search for an entity index. This won't be 100% reliable if the source
// talker dies and another entity is created immediately afterwards, but it's a lot more reliable than a simple entity name search.
criteria.AppendCriteria( "From_idx", CNumStr( pOuter->entindex() ) );
#endif
criteria.Merge( criteriaStr );
g_ResponseQueueManager.GetQueue()->Add( concept, &criteria, gpGlobals->curtime + delay, target, pOuter );
@ -436,6 +462,12 @@ void CAI_ExpresserWithFollowup::OnSpeechFinished()
{
if (m_pPostponedFollowup && m_pPostponedFollowup->IsValid())
{
#ifdef MAPBASE
// HACKHACK: Non-scene speech (e.g. noscene speak/sentence) fire OnSpeechFinished() immediately,
// so add the actual speech time to the followup delay
if (GetTimeSpeechCompleteWithoutDelay() > gpGlobals->curtime)
m_pPostponedFollowup->followup_delay += GetTimeSpeechCompleteWithoutDelay() - gpGlobals->curtime;
#endif
return SpeakDispatchFollowup(*m_pPostponedFollowup);
}
}

View File

@ -750,6 +750,10 @@ bool CAI_Expresser::SpeakDispatchResponse( AIConcept_t &concept, AI_Response *re
DevMsg( 2, "SpeakDispatchResponse: Entity ( %i/%s ) playing sound '%s'\n", GetOuter()->entindex(), STRING( GetOuter()->GetEntityName() ), response );
NoteSpeaking( speakTime, delay );
spoke = true;
#ifdef MAPBASE
// Not really any other way of doing this
OnSpeechFinished();
#endif
}
}
break;
@ -757,6 +761,10 @@ bool CAI_Expresser::SpeakDispatchResponse( AIConcept_t &concept, AI_Response *re
case ResponseRules::RESPONSE_SENTENCE:
{
spoke = ( -1 != SpeakRawSentence( response, delay, VOL_NORM, soundlevel ) ) ? true : false;
#ifdef MAPBASE
// Not really any other way of doing this
OnSpeechFinished();
#endif
}
break;
@ -781,17 +789,30 @@ bool CAI_Expresser::SpeakDispatchResponse( AIConcept_t &concept, AI_Response *re
NDebugOverlay::Text( vPrintPos, response, true, 1.5 );
}
spoke = true;
#ifdef MAPBASE
OnSpeechFinished();
#endif
}
break;
case ResponseRules::RESPONSE_ENTITYIO:
{
return FireEntIOFromResponse( response, GetOuter() );
spoke = FireEntIOFromResponse( response, GetOuter() );
#ifdef MAPBASE
OnSpeechFinished();
#endif
}
break;
#ifdef MAPBASE
#ifdef MAPBASE_VSCRIPT
case ResponseRules::RESPONSE_VSCRIPT:
{
return GetOuter()->RunScript( response, "ResponseScript" );
spoke = RunScriptResponse( GetOuter(), response, criteria, false );
OnSpeechFinished();
}
break;
case ResponseRules::RESPONSE_VSCRIPT_FILE:
{
spoke = RunScriptResponse( GetOuter(), response, criteria, true );
OnSpeechFinished();
}
break;
#endif
@ -918,6 +939,47 @@ bool CAI_Expresser::FireEntIOFromResponse( char *response, CBaseEntity *pInitiat
return true;
}
#ifdef MAPBASE_VSCRIPT
bool CAI_Expresser::RunScriptResponse( CBaseEntity *pTarget, const char *response, AI_CriteriaSet *criteria, bool file )
{
if (!pTarget->ValidateScriptScope())
return false;
ScriptVariant_t varCriteriaTable;
g_pScriptVM->CreateTable( varCriteriaTable );
if (criteria)
{
// Sort all of the criteria into a table.
// Letting VScript have access to this is important because not all criteria is appended in ModifyOrAppendCriteria() and
// not all contexts are actually appended as contexts. This is specifically important for followup responses.
int count = criteria->GetCount();
for ( int i = 0 ; i < count ; ++i )
{
// TODO: Weight?
g_pScriptVM->SetValue( varCriteriaTable, criteria->GetName(i), criteria->GetValue(i) );
}
g_pScriptVM->SetValue( "criteria", varCriteriaTable );
}
bool bSuccess = false;
if (file)
{
bSuccess = pTarget->RunScriptFile( response );
}
else
{
bSuccess = pTarget->RunScript( response, "ResponseScript" );
}
g_pScriptVM->ClearValue( "criteria" );
g_pScriptVM->ReleaseScript( varCriteriaTable );
return bSuccess;
}
#endif
//-----------------------------------------------------------------------------
// Purpose:
// Input : *response -
@ -967,6 +1029,37 @@ float CAI_Expresser::GetResponseDuration( AI_Response *result )
return 0.0f;
}
#ifdef MAPBASE
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CAI_Expresser::SetUsingProspectiveResponses( bool bToggle )
{
VPROF("CAI_Expresser::SetUsingProspectiveResponses");
IResponseSystem *rs = GetOuter()->GetResponseSystem();
if ( !rs )
{
Assert( !"No response system installed for CAI_Expresser::GetOuter()!!!" );
return;
}
rs->SetProspective( bToggle );
}
void CAI_Expresser::MarkResponseAsUsed( AI_Response *response )
{
VPROF("CAI_Expresser::MarkResponseAsUsed");
IResponseSystem *rs = GetOuter()->GetResponseSystem();
if ( !rs )
{
Assert( !"No response system installed for CAI_Expresser::GetOuter()!!!" );
return;
}
rs->MarkResponseAsUsed( response->GetInternalIndices()[0], response->GetInternalIndices()[1] );
}
#endif
//-----------------------------------------------------------------------------
// Purpose: Placeholder for rules based response system
// Input : concept -

View File

@ -178,6 +178,11 @@ public:
virtual bool SpeakDispatchResponse( AIConcept_t &concept, AI_Response *response, AI_CriteriaSet *criteria, IRecipientFilter *filter = NULL );
float GetResponseDuration( AI_Response *response );
#ifdef MAPBASE
void SetUsingProspectiveResponses( bool bToggle );
void MarkResponseAsUsed( AI_Response *response );
#endif
virtual int SpeakRawSentence( const char *pszSentence, float delay, float volume = VOL_NORM, soundlevel_t soundlevel = SNDLVL_TALKING, CBaseEntity *pListener = NULL );
bool SemaphoreIsAvailable( CBaseEntity *pTalker );
@ -194,6 +199,9 @@ public:
bool CanSpeak();
bool CanSpeakAfterMyself();
float GetTimeSpeechComplete() const { return m_flStopTalkTime; }
#ifdef MAPBASE
float GetTimeSpeechCompleteWithoutDelay() const { return m_flStopTalkTimeWithoutDelay; }
#endif
void BlockSpeechUntil( float time );
// --------------------------------
@ -227,6 +235,11 @@ public:
// returns false on failure (eg, couldn't match parse contents)
static bool FireEntIOFromResponse( char *response, CBaseEntity *pInitiator );
#ifdef MAPBASE_VSCRIPT
// Used for RESPONSE_VSCRIPT(_FILE)
static bool RunScriptResponse( CBaseEntity *pTarget, const char *response, AI_CriteriaSet *criteria, bool file );
#endif
protected:
CAI_TimedSemaphore *GetMySpeechSemaphore( CBaseEntity *pNpc );

View File

@ -374,6 +374,10 @@ bool CResponseQueue::DispatchOneResponse_ThenANY( CDeferredResponse &response, A
}
AI_Response prospectiveResponse;
#ifdef MAPBASE
pEx->SetUsingProspectiveResponses( true );
#endif
if ( pEx->FindResponse( prospectiveResponse, response.m_concept, &characterCriteria ) )
{
float score = prospectiveResponse.GetMatchScore();
@ -390,8 +394,13 @@ bool CResponseQueue::DispatchOneResponse_ThenANY( CDeferredResponse &response, A
}
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 )
if ( numExFound >= EXARRAYMAX )
{
#ifdef MAPBASE
pEx->SetUsingProspectiveResponses( false );
#endif
continue; // SAFETY: don't overflow the array
}
responseToSay[numExFound] = prospectiveResponse;
pBestEx[numExFound] = pEx;
@ -400,6 +409,10 @@ bool CResponseQueue::DispatchOneResponse_ThenANY( CDeferredResponse &response, A
}
}
}
#ifdef MAPBASE
pEx->SetUsingProspectiveResponses( false );
#endif
}
// if I have a response, dispatch it.
@ -410,6 +423,9 @@ bool CResponseQueue::DispatchOneResponse_ThenANY( CDeferredResponse &response, A
if ( pBestEx[iSelect] != NULL )
{
#ifdef MAPBASE
pBestEx[iSelect]->MarkResponseAsUsed( responseToSay + iSelect );
#endif
return pBestEx[iSelect]->SpeakDispatchResponse( response.m_concept, responseToSay + iSelect, pDeferredCriteria );
}
else

View File

@ -98,6 +98,7 @@ namespace ResponseRules
RESPONSE_ENTITYIO, // poke an input on an entity
#ifdef MAPBASE
RESPONSE_VSCRIPT, // Run VScript code
RESPONSE_VSCRIPT_FILE, // Run a VScript file (bypasses ugliness and character limits when just using IncludeScript() with RESPONSE_VSCRIPT)
#endif
NUM_RESPONSES,
@ -351,6 +352,9 @@ namespace ResponseRules
#ifdef MAPBASE
int GetContextFlags() { return m_iContextFlags; }
bool IsApplyContextToWorld( void ) { return (m_iContextFlags & APPLYCONTEXT_WORLD) != 0; }
inline short *GetInternalIndices() { return m_InternalIndices; }
inline void SetInternalIndices( short iGroup, short iWithinGroup ) { m_InternalIndices[0] = iGroup; m_InternalIndices[1] = iWithinGroup; }
#else
bool IsApplyContextToWorld( void ) { return m_bApplyContextToWorld; }
#endif
@ -393,6 +397,10 @@ namespace ResponseRules
char * m_szContext; // context data we apply to character after running
#ifdef MAPBASE
int m_iContextFlags;
// The response's original indices in the system. [0] is the group's index, [1] is the index within the group.
// For now, this is only set in prospecctive mode. It's used to call back to the ParserResponse and mark a prospectively chosen response as used.
short m_InternalIndices[2];
#else
bool m_bApplyContextToWorld;
#endif
@ -418,6 +426,15 @@ namespace ResponseRules
virtual bool FindBestResponse( const CriteriaSet& set, CRR_Response& response, IResponseFilter *pFilter = NULL ) = 0;
virtual void GetAllResponses( CUtlVector<CRR_Response> *pResponses ) = 0;
virtual void PrecacheResponses( bool bEnable ) = 0;
#ifdef MAPBASE
// (Optional) Call this before and after using FindBestResponse() for a prospective lookup, e.g. a response that might not actually be used
// and should not trigger displayfirst, etc.
virtual void SetProspective( bool bToggle ) {};
// (Optional) Marks a prospective response as used
virtual void MarkResponseAsUsed( short iGroup, short iWithinGroup ) {};
#endif
};

View File

@ -942,6 +942,10 @@ int CResponseSystem::SelectWeightedResponseFromResponseGroup( ResponseGroup *g,
}
if ( slot != -1 )
#ifdef MAPBASE
// Don't mark responses as used in prospective mode
if (m_bInProspective == false)
#endif
g->MarkResponseUsed( slot );
// Revert fake depletion of unavailable choices
@ -1284,6 +1288,26 @@ bool CResponseSystem::FindBestResponse( const CriteriaSet& set, CRR_Response& re
context = r->GetContext();
#ifdef MAPBASE
contextflags = r->GetContextFlags();
// Sets the internal indices for the response to call back to later for prospective responses
// (NOTE: Performance not tested; Be wary of turning off the m_bInProspective check!)
if (m_bInProspective)
{
for ( int i = 0; i < (int)m_Responses.Count(); i++ )
{
if (&m_Responses[i] == result.group)
{
ResponseGroup &group = m_Responses[i];
for ( int j = 0; j < group.group.Count(); j++)
{
if (&group.group[j] == result.action)
{
response.SetInternalIndices( i, j );
}
}
}
}
}
#else
bcontexttoworld = r->IsApplyContextToWorld();
#endif
@ -1369,6 +1393,22 @@ void CResponseSystem::GetAllResponses( CUtlVector<CRR_Response> *pResponses )
}
}
#ifdef MAPBASE
void CResponseSystem::MarkResponseAsUsed( short iGroup, short iWithinGroup )
{
if (m_Responses.Count() > (unsigned int)iGroup)
{
ResponseGroup &group = m_Responses[iGroup];
if (group.group.Count() > (int)iWithinGroup)
{
group.MarkResponseUsed( iWithinGroup );
Msg("Marked response %s (%i) used\n", group.group[iWithinGroup].value, iWithinGroup);
}
}
}
#endif
void CResponseSystem::ParseInclude()
{
char includefile[ 256 ];
@ -1521,7 +1561,10 @@ inline ResponseType_t ComputeResponseType( const char *s )
return RESPONSE_ENTITYIO;
#ifdef MAPBASE
case 'v':
return RESPONSE_VSCRIPT;
if (*(s + 7) == '_')
return RESPONSE_VSCRIPT_FILE;
else
return RESPONSE_VSCRIPT;
#endif
}

View File

@ -44,6 +44,12 @@ namespace ResponseRules
// IResponseSystem
virtual bool FindBestResponse( const CriteriaSet& set, CRR_Response& response, IResponseFilter *pFilter = NULL );
virtual void GetAllResponses( CUtlVector<CRR_Response> *pResponses );
#ifdef MAPBASE
virtual void SetProspective( bool bToggle ) { m_bInProspective = bToggle; }
virtual void MarkResponseAsUsed( short iGroup, short iWithinGroup );
#endif
#pragma endregion Implement interface from IResponseSystem
virtual void Release() = 0;
@ -283,6 +289,13 @@ public:
bool m_bCustomManagable;
#ifdef MAPBASE
// This is a hack specifically designed to fix displayfirst, speakonce, etc. in "prospective" response searches,
// especially the prospective lookups in followup responses.
// It works by preventing responses from being marked as "used".
bool m_bInProspective;
#endif
struct ScriptEntry
{
unsigned char *buffer;

View File

@ -244,6 +244,8 @@ const char *CRR_Response::DescribeResponse( ResponseType_t type )
#ifdef MAPBASE
case ResponseRules::RESPONSE_VSCRIPT:
return "RESPONSE_VSCRIPT";
case ResponseRules::RESPONSE_VSCRIPT_FILE:
return "RESPONSE_VSCRIPT_FILE";
#endif
}