From d4a91fe02715d9d5fac1718027b04c00b50bd8fe Mon Sep 17 00:00:00 2001 From: Blixibon Date: Tue, 9 Mar 2021 10:03:40 -0600 Subject: [PATCH] Made followup responses more reliable with generic NPCs and added "vscript_file" response type --- sp/src/game/server/ai_expresserfollowup.cpp | 34 ++++++- sp/src/game/server/ai_speech_new.cpp | 99 ++++++++++++++++++- sp/src/game/server/ai_speech_new.h | 13 +++ sp/src/game/server/ai_speechqueue.cpp | 18 +++- sp/src/public/responserules/response_types.h | 17 ++++ .../responserules/runtime/response_system.cpp | 45 ++++++++- .../responserules/runtime/response_system.h | 13 +++ sp/src/responserules/runtime/rr_response.cpp | 2 + 8 files changed, 235 insertions(+), 6 deletions(-) diff --git a/sp/src/game/server/ai_expresserfollowup.cpp b/sp/src/game/server/ai_expresserfollowup.cpp index ff49d535..84db4773 100644 --- a/sp/src/game/server/ai_expresserfollowup.cpp +++ b/sp/src/game/server/ai_expresserfollowup.cpp @@ -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); } } diff --git a/sp/src/game/server/ai_speech_new.cpp b/sp/src/game/server/ai_speech_new.cpp index f6cfaaaa..a7c4ac72 100644 --- a/sp/src/game/server/ai_speech_new.cpp +++ b/sp/src/game/server/ai_speech_new.cpp @@ -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 - diff --git a/sp/src/game/server/ai_speech_new.h b/sp/src/game/server/ai_speech_new.h index 02501c18..e3c895be 100644 --- a/sp/src/game/server/ai_speech_new.h +++ b/sp/src/game/server/ai_speech_new.h @@ -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 ); diff --git a/sp/src/game/server/ai_speechqueue.cpp b/sp/src/game/server/ai_speechqueue.cpp index 46779ad6..79794d4b 100644 --- a/sp/src/game/server/ai_speechqueue.cpp +++ b/sp/src/game/server/ai_speechqueue.cpp @@ -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 diff --git a/sp/src/public/responserules/response_types.h b/sp/src/public/responserules/response_types.h index 2e7472c2..821b0c57 100644 --- a/sp/src/public/responserules/response_types.h +++ b/sp/src/public/responserules/response_types.h @@ -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 *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 }; diff --git a/sp/src/responserules/runtime/response_system.cpp b/sp/src/responserules/runtime/response_system.cpp index afdc40ef..f517c5a6 100644 --- a/sp/src/responserules/runtime/response_system.cpp +++ b/sp/src/responserules/runtime/response_system.cpp @@ -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 *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 } diff --git a/sp/src/responserules/runtime/response_system.h b/sp/src/responserules/runtime/response_system.h index 5fec10de..9849b5a9 100644 --- a/sp/src/responserules/runtime/response_system.h +++ b/sp/src/responserules/runtime/response_system.h @@ -44,6 +44,12 @@ namespace ResponseRules // IResponseSystem virtual bool FindBestResponse( const CriteriaSet& set, CRR_Response& response, IResponseFilter *pFilter = NULL ); virtual void GetAllResponses( CUtlVector *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; diff --git a/sp/src/responserules/runtime/rr_response.cpp b/sp/src/responserules/runtime/rr_response.cpp index a1ff5a8b..6124805c 100644 --- a/sp/src/responserules/runtime/rr_response.cpp +++ b/sp/src/responserules/runtime/rr_response.cpp @@ -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 }