From fd149ff1616b2dd6c923024b700346de4908c5b5 Mon Sep 17 00:00:00 2001 From: "ALLEN-PC\\acj30" Date: Wed, 3 Jan 2024 13:19:38 -0600 Subject: [PATCH] Add new dynamic interaction keyvalues and utilities --- sp/src/game/server/ai_activity.cpp | 17 ++ sp/src/game/server/ai_basenpc.cpp | 285 +++++++++++++++++++++++------ sp/src/game/server/ai_basenpc.h | 33 +++- 3 files changed, 270 insertions(+), 65 deletions(-) diff --git a/sp/src/game/server/ai_activity.cpp b/sp/src/game/server/ai_activity.cpp index da12eabe..e20bfc48 100644 --- a/sp/src/game/server/ai_activity.cpp +++ b/sp/src/game/server/ai_activity.cpp @@ -76,6 +76,23 @@ int CAI_BaseNPC::GetActivityID(const char* actName) return m_pActivitySR->GetStringID(actName); } +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Gets an activity ID or registers a new private one if it doesn't exist +//----------------------------------------------------------------------------- +int CAI_BaseNPC::GetOrRegisterActivity( const char *actName ) +{ + int actID = GetActivityID( actName ); + if (actID == ACT_INVALID) + { + actID = ActivityList_RegisterPrivateActivity( actName ); + AddActivityToSR( actName, actID ); + } + + return actID; +} +#endif + #define ADD_ACTIVITY_TO_SR(activityname) AddActivityToSR(#activityname,activityname) //----------------------------------------------------------------------------- diff --git a/sp/src/game/server/ai_basenpc.cpp b/sp/src/game/server/ai_basenpc.cpp index acfebf14..acb62cc8 100644 --- a/sp/src/game/server/ai_basenpc.cpp +++ b/sp/src/game/server/ai_basenpc.cpp @@ -12443,7 +12443,11 @@ BEGIN_SIMPLE_DATADESC( ScriptedNPCInteraction_t ) DEFINE_FIELD( bValidOnCurrentEnemy, FIELD_BOOLEAN ), DEFINE_FIELD( flNextAttemptTime, FIELD_TIME ), #ifdef MAPBASE - DEFINE_FIELD( MiscCriteria, FIELD_STRING ),//DEFINE_UTLVECTOR( MiscCriteria, FIELD_EMBEDDED ), + DEFINE_EMBEDDED_ARRAY( sTheirPhases, SNPCINT_NUM_PHASES ), + DEFINE_FIELD( bHasSeparateSequenceNames, FIELD_BOOLEAN ), + DEFINE_FIELD( flMaxAngleDiff, FIELD_FLOAT ), + DEFINE_FIELD( iszRelatedInteractions, FIELD_STRING ), + DEFINE_FIELD( MiscCriteria, FIELD_STRING ), #endif END_DATADESC() @@ -14847,32 +14851,60 @@ void CAI_BaseNPC::ParseScriptedNPCInteractions(void) else if (!Q_strncmp(szName, "entry_sequence", 14)) sInteraction.sPhases[SNPCINT_ENTRY].iszSequence = AllocPooledString(szValue); else if (!Q_strncmp(szName, "entry_activity", 14)) - sInteraction.sPhases[SNPCINT_ENTRY].iActivity = GetActivityID(szValue); + sInteraction.sPhases[SNPCINT_ENTRY].iActivity = GetOrRegisterActivity(szValue); else if (!Q_strncmp(szName, "sequence", 8)) sInteraction.sPhases[SNPCINT_SEQUENCE].iszSequence = AllocPooledString(szValue); else if (!Q_strncmp(szName, "activity", 8)) - sInteraction.sPhases[SNPCINT_SEQUENCE].iActivity = GetActivityID(szValue); + sInteraction.sPhases[SNPCINT_SEQUENCE].iActivity = GetOrRegisterActivity(szValue); else if (!Q_strncmp(szName, "exit_sequence", 13)) sInteraction.sPhases[SNPCINT_EXIT].iszSequence = AllocPooledString(szValue); else if (!Q_strncmp(szName, "exit_activity", 13)) - sInteraction.sPhases[SNPCINT_EXIT].iActivity = GetActivityID(szValue); + sInteraction.sPhases[SNPCINT_EXIT].iActivity = GetOrRegisterActivity(szValue); + + else if (!Q_strncmp(szName, "their_", 6)) + { + szName += 6; + sInteraction.bHasSeparateSequenceNames = true; + + if (!Q_strncmp(szName, "entry_sequence", 14)) + sInteraction.sTheirPhases[SNPCINT_ENTRY].iszSequence = AllocPooledString(szValue); + else if (!Q_strncmp(szName, "entry_activity", 14)) + sInteraction.sTheirPhases[SNPCINT_ENTRY].iActivity = GetOrRegisterActivity(szValue); + + else if (!Q_strncmp(szName, "sequence", 8)) + sInteraction.sTheirPhases[SNPCINT_SEQUENCE].iszSequence = AllocPooledString(szValue); + else if (!Q_strncmp(szName, "activity", 8)) + sInteraction.sTheirPhases[SNPCINT_SEQUENCE].iActivity = GetOrRegisterActivity(szValue); + + else if (!Q_strncmp(szName, "exit_sequence", 13)) + sInteraction.sTheirPhases[SNPCINT_EXIT].iszSequence = AllocPooledString(szValue); + else if (!Q_strncmp(szName, "exit_activity", 13)) + sInteraction.sTheirPhases[SNPCINT_EXIT].iActivity = GetOrRegisterActivity(szValue); + } else if (!Q_strncmp(szName, "delay", 5)) sInteraction.flDelay = atof(szValue); else if (!Q_strncmp(szName, "origin_max_delta", 16)) sInteraction.flDistSqr = atof(szValue); + else if (!Q_strncmp(szName, "angles_max_diff", 15)) + sInteraction.flMaxAngleDiff = atof(szValue); else if (!Q_strncmp(szName, "loop_in_action", 14) && !FStrEq(szValue, "0")) sInteraction.iFlags |= SCNPC_FLAG_LOOP_IN_ACTION; else if (!Q_strncmp(szName, "dont_teleport_at_end", 20)) { - if (!Q_stricmp(szValue, "me") || !Q_stricmp(szValue, "both")) + if (!Q_stricmp(szValue, "me")) sInteraction.iFlags |= SCNPC_FLAG_DONT_TELEPORT_AT_END_ME; - else if (!Q_stricmp(szValue, "them") || !Q_stricmp(szValue, "both")) + else if (!Q_stricmp(szValue, "them")) sInteraction.iFlags |= SCNPC_FLAG_DONT_TELEPORT_AT_END_THEM; + else if (!Q_stricmp( szValue, "both" )) + { + sInteraction.iFlags |= SCNPC_FLAG_DONT_TELEPORT_AT_END_ME; + sInteraction.iFlags |= SCNPC_FLAG_DONT_TELEPORT_AT_END_THEM; + } } else if (!Q_strncmp(szName, "needs_weapon", 12)) @@ -14899,6 +14931,11 @@ void CAI_BaseNPC::ParseScriptedNPCInteractions(void) sInteraction.iszTheirWeapon = AllocPooledString(szValue); } + else if (!Q_strncmp(szName, "related_interactions", 20)) + { + sInteraction.iszRelatedInteractions = AllocPooledString(szValue); + } + // Add anything else to our miscellaneous criteria else { @@ -15130,8 +15167,23 @@ void CAI_BaseNPC::AddScriptedNPCInteraction( ScriptedNPCInteraction_t *pInteract //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- -const char *CAI_BaseNPC::GetScriptedNPCInteractionSequence( ScriptedNPCInteraction_t *pInteraction, int iPhase ) +const char *CAI_BaseNPC::GetScriptedNPCInteractionSequence( ScriptedNPCInteraction_t *pInteraction, int iPhase, bool bOtherNPC ) { +#ifdef MAPBASE + if (bOtherNPC && pInteraction->bHasSeparateSequenceNames) + { + // Check unique phases + if ( pInteraction->sTheirPhases[iPhase].iActivity != ACT_INVALID ) + { + int iSequence = SelectWeightedSequence( (Activity)pInteraction->sTheirPhases[iPhase].iActivity ); + return GetSequenceName( iSequence ); + } + + if ( pInteraction->sTheirPhases[iPhase].iszSequence != NULL_STRING ) + return STRING(pInteraction->sTheirPhases[iPhase].iszSequence); + } +#endif + if ( pInteraction->sPhases[iPhase].iActivity != ACT_INVALID ) { int iSequence = SelectWeightedSequence( (Activity)pInteraction->sPhases[iPhase].iActivity ); @@ -15225,6 +15277,36 @@ void CAI_BaseNPC::StartScriptedNPCInteraction( CAI_BaseNPC *pOtherNPC, ScriptedN // Setup next attempt pInteraction->flNextAttemptTime = gpGlobals->curtime + pInteraction->flDelay + RandomFloat(-2,2); +#ifdef MAPBASE + if (pInteraction->iszRelatedInteractions != NULL_STRING) + { + // Delay related interactions as well + char szRelatedInteractions[256]; + Q_strncpy( szRelatedInteractions, STRING( pInteraction->iszRelatedInteractions ), sizeof( szRelatedInteractions ) ); + + char *pszInteraction = strtok( szRelatedInteractions, "," ); + while (pszInteraction) + { + bool bWildCard = Matcher_ContainsWildcard( pszInteraction ); + + for ( int i = 0; i < m_ScriptedInteractions.Count(); i++ ) + { + ScriptedNPCInteraction_t *pOtherInteraction = &m_ScriptedInteractions[i]; + + if ( Matcher_NamesMatch( pszInteraction, STRING( pOtherInteraction->iszInteractionName ) ) && pOtherInteraction != pInteraction ) + { + pOtherInteraction->flNextAttemptTime = pInteraction->flNextAttemptTime; + + // Not looking for multiple + if (!bWildCard) + break; + } + } + + pszInteraction = strtok( NULL, "," ); + } + } +#endif // Spawn a scripted sequence for this NPC to play the interaction anim CAI_ScriptedSequence *pMySequence = (CAI_ScriptedSequence*)CreateEntityByName( "scripted_sequence" ); @@ -15256,6 +15338,15 @@ void CAI_BaseNPC::StartScriptedNPCInteraction( CAI_BaseNPC *pOtherNPC, ScriptedN CAI_ScriptedSequence *pTheirSequence = NULL; if ( pOtherNPC ) { +#ifdef MAPBASE + if (pInteraction->bHasSeparateSequenceNames) + { + pszEntrySequence = GetScriptedNPCInteractionSequence( pInteraction, SNPCINT_ENTRY, true ); + pszSequence = GetScriptedNPCInteractionSequence( pInteraction, SNPCINT_SEQUENCE, true ); + pszExitSequence = GetScriptedNPCInteractionSequence( pInteraction, SNPCINT_EXIT, true ); + } +#endif + pTheirSequence = (CAI_ScriptedSequence*)CreateEntityByName( "scripted_sequence" ); pTheirSequence->KeyValue( "m_iszEntry", pszEntrySequence ); pTheirSequence->KeyValue( "m_iszPlay", pszSequence ); @@ -15367,7 +15458,7 @@ bool CAI_BaseNPC::CanRunAScriptedNPCInteraction( bool bForced ) return false; // Default AI prevents interactions while melee attacking, but not ranged attacking - if ( IsCurSchedule( SCHED_MELEE_ATTACK1 ) || IsCurSchedule( SCHED_MELEE_ATTACK2 ) ) + if ( ( IsCurSchedule( SCHED_MELEE_ATTACK1 ) || IsCurSchedule( SCHED_MELEE_ATTACK2 ) ) && !CanStartDynamicInteractionDuringMelee() ) return false; } @@ -15556,8 +15647,14 @@ void CAI_BaseNPC::CalculateValidEnemyInteractions( void ) if (bSame) continue; -#endif + // Resolve the activity or sequence, and make sure our enemy has it + const char *pszSequence = GetScriptedNPCInteractionSequence( pInteraction, SNPCINT_SEQUENCE, true ); + if ( !pszSequence ) + continue; + if ( pNPC->LookupSequence( pszSequence ) == -1 ) + continue; +#else // Use sequence? or activity? if ( pInteraction->sPhases[SNPCINT_SEQUENCE].iActivity != ACT_INVALID ) { @@ -15573,53 +15670,6 @@ void CAI_BaseNPC::CalculateValidEnemyInteractions( void ) if ( pNPC->LookupSequence( STRING(pInteraction->sPhases[SNPCINT_SEQUENCE].iszSequence) ) == -1 ) continue; } - -#ifdef MAPBASE - if (pInteraction->MiscCriteria != NULL_STRING) - { - // Test against response system criteria - AI_CriteriaSet set; - ModifyOrAppendCriteria( set ); - CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); - if( pPlayer ) - pPlayer->ModifyOrAppendPlayerCriteria( set ); - ReAppendContextCriteria( set ); - - DevMsg("Testing %s misc criteria\n", STRING(pInteraction->MiscCriteria)); - - int index; - const char *criteriavalue; - char key[128]; - char value[128]; - const char *p = STRING(pInteraction->MiscCriteria); - while ( p ) - { -#ifdef NEW_RESPONSE_SYSTEM - p = SplitContext( p, key, sizeof( key ), value, sizeof( value ), NULL, STRING(pInteraction->MiscCriteria) ); -#else - p = SplitContext( p, key, sizeof( key ), value, sizeof( value ), NULL ); -#endif - - index = set.FindCriterionIndex(key); - if (index != -1) - { - criteriavalue = set.GetValue(index); - if (!Matcher_Match(value, criteriavalue)) - { - continue; - } - } - else - { - // Test with empty string in case our criteria is != or something - criteriavalue = ""; - if (!Matcher_Match(value, criteriavalue)) - { - continue; - } - } - } - } #endif pInteraction->bValidOnCurrentEnemy = true; @@ -15789,7 +15839,95 @@ bool CAI_BaseNPC::InteractionIsAllowed( CAI_BaseNPC *pOtherNPC, ScriptedNPCInter return true; // m_iDynamicInteractionsAllowed == TRS_FALSE case is already handled in CanRunAScriptedNPCInteraction(). - return !(pInteraction->iFlags & SCNPC_FLAG_MAPBASE_ADDITION && m_iDynamicInteractionsAllowed == TRS_NONE); + if (pInteraction->iFlags & SCNPC_FLAG_MAPBASE_ADDITION && m_iDynamicInteractionsAllowed == TRS_NONE) + return false; + + // Test misc. criteria here since some of it may not have been valid on initial calculation, but could be now + if (pInteraction->MiscCriteria != NULL_STRING) + { + // Test against response system criteria + AI_CriteriaSet set; + ModifyOrAppendCriteria( set ); + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if (pPlayer) + pPlayer->ModifyOrAppendPlayerCriteria( set ); + + // Get criteria from target if we want it + if ( V_strstr( STRING( pInteraction->MiscCriteria ), "their_" ) ) + { + // Currently, in order to get everything which might be desired, we call the other NPC's ModifyOrAppendCriteria. + // We put it in a separate criteria set, then assign a prefix and append it to the main set, similar to how contexts are appended. + // This includes a few global criterions which we might not need, so we throw them out before they're merged. + // This isn't a very efficient solution, but there are no better options available without rewriting parts of the response criteria system. + AI_CriteriaSet theirSet; + pOtherNPC->ModifyOrAppendCriteria( theirSet ); + + set.EnsureCapacity( (theirSet.GetCount()-2) + set.GetCount() ); // We know we'll be throwing out 2 global criterions + + char sz[ 128 ]; + for ( int i = 0; i < theirSet.GetCount(); i++ ) + { + const char *name = theirSet.GetName( i ); + const char *value = theirSet.GetValue( i ); + + if (FStrEq( name, "map" ) || FStrEq( name, "episodic" ) || FStrEq( name, "is_console" ) + || FStrEq( name, "month" ) || FStrEq( name, "day" ) + || FStrEq( name, "is_console" ) || FStrEq( name, "is_pc" ) + || V_strnicmp( name, "world", 5 ) == 0) + { + // Global criterion, ignore + continue; + } + + Q_snprintf( sz, sizeof( sz ), "their_%s", name ); + + if (ai_debug_dyninteractions.GetInt() == 3) + Msg( "%i: %s -> %s:%s\n", i, name, sz, value ); + + set.AppendCriteria( sz, value ); + } + + // Append this afterwards because it has its own prefix system + pOtherNPC->AppendContextToCriteria( set, "their_" ); + } + + ReAppendContextCriteria( set ); + + int index; + const char *criteriavalue; + char key[128]; + char value[128]; + const char *p = STRING( pInteraction->MiscCriteria ); + while ( p ) + { +#ifdef NEW_RESPONSE_SYSTEM + p = SplitContext( p, key, sizeof( key ), value, sizeof( value ), NULL, STRING( pInteraction->MiscCriteria ) ); +#else + p = SplitContext( p, key, sizeof( key ), value, sizeof( value ), NULL ); +#endif + + index = set.FindCriterionIndex( key ); + if (index != -1) + { + criteriavalue = set.GetValue( index ); + if (!Matcher_Match( value, criteriavalue )) + { + return false; + } + } + else + { + // Test with empty string in case our criteria is != or something + criteriavalue = ""; + if (!Matcher_Match( value, criteriavalue )) + { + return false; + } + } + } + } + + return true; } #endif @@ -15855,14 +15993,28 @@ bool CAI_BaseNPC::InteractionCouldStart( CAI_BaseNPC *pOtherNPC, ScriptedNPCInte for ( int ang = 0; ang < 3; ang++ ) { float flAngDiff = AngleDiff( angEnemyAngles[ang], angAngles[ang] ); +#ifdef MAPBASE + if ( fabs(flAngDiff) > pInteraction->flMaxAngleDiff ) +#else if ( fabs(flAngDiff) > DSS_MAX_ANGLE_DIFF ) +#endif { bMatches = false; break; } } if ( !bMatches ) + { +#ifdef MAPBASE + if ( bDebug ) + { + Msg(" %s angle not matched: (%0.2f %0.2f %0.2f), desired (%0.2f, %0.2f, %0.2f)\n", GetDebugName(), + anglemod(angEnemyAngles.x), anglemod(angEnemyAngles.y), anglemod(angEnemyAngles.z), anglemod(angAngles.x), anglemod(angAngles.y), anglemod(angAngles.z) ); + Msg(" diff: (%0.2f, %0.2f, %0.2f)\n", AngleDiff( angEnemyAngles.x, angAngles.x ), AngleDiff( angEnemyAngles.y, angAngles.y ), AngleDiff( angEnemyAngles.z, angAngles.z ) ); + } +#endif return false; + } if ( bDebug ) { @@ -15980,6 +16132,25 @@ bool CAI_BaseNPC::HasInteractionCantDie( void ) return ( m_bCannotDieDuringInteraction && IsRunningDynamicInteraction() ); } +//----------------------------------------------------------------------------- +// Purpose: Return true if this NPC has valid interactions on the current enemy. +//----------------------------------------------------------------------------- +bool CAI_BaseNPC::HasValidInteractionsOnCurrentEnemy( void ) +{ + if ( !GetEnemy() || !GetEnemy()->IsNPC() ) + return false; + + for ( int i = 0; i < m_ScriptedInteractions.Count(); i++ ) + { + ScriptedNPCInteraction_t *pInteraction = &m_ScriptedInteractions[i]; + + if ( pInteraction->bValidOnCurrentEnemy ) + return true; + } + + return false; +} + //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - diff --git a/sp/src/game/server/ai_basenpc.h b/sp/src/game/server/ai_basenpc.h index 878a6f81..5aa4ed7b 100644 --- a/sp/src/game/server/ai_basenpc.h +++ b/sp/src/game/server/ai_basenpc.h @@ -425,6 +425,9 @@ struct ScriptedNPCInteraction_t iszTheirWeapon = NULL_STRING; #ifdef MAPBASE vecRelativeEndPos = vec3_origin; + bHasSeparateSequenceNames = false; + flMaxAngleDiff = DSS_MAX_ANGLE_DIFF; + iszRelatedInteractions = NULL_STRING; MiscCriteria = NULL_STRING; #endif @@ -432,6 +435,10 @@ struct ScriptedNPCInteraction_t { sPhases[i].iszSequence = NULL_STRING; sPhases[i].iActivity = ACT_INVALID; +#ifdef MAPBASE + sTheirPhases[i].iszSequence = NULL_STRING; + sTheirPhases[i].iActivity = ACT_INVALID; +#endif } } @@ -459,10 +466,14 @@ struct ScriptedNPCInteraction_t float flNextAttemptTime; #ifdef MAPBASE - // Unrecognized keyvalues are tested against response criteria later. - // This was originally a CUtlVector that carries response contexts, but I couldn't get it working due to some CUtlVector-struct shenanigans. - // It works when we use a single string_t that's split and read each time the code runs, but feel free to improve on this. - string_t MiscCriteria; // CUtlVector + ScriptedNPCInteraction_Phases_t sTheirPhases[SNPCINT_NUM_PHASES]; // The animations played by the target NPC, if they are different + bool bHasSeparateSequenceNames; + + float flMaxAngleDiff; + string_t iszRelatedInteractions; // These interactions will be delayed as well when this interaction is used. + + // Unrecognized keyvalues which are tested against response criteria later. + string_t MiscCriteria; #endif DECLARE_SIMPLE_DATADESC(); @@ -1304,10 +1315,14 @@ private: public: float GetInteractionYaw( void ) const { return m_flInteractionYaw; } + bool IsRunningDynamicInteraction( void ) { return (m_iInteractionState != NPCINT_NOT_RUNNING && (m_hCine != NULL)); } + bool IsActiveDynamicInteraction( void ) { return (m_iInteractionState == NPCINT_RUNNING_ACTIVE && (m_hCine != NULL)); } + CAI_BaseNPC *GetInteractionPartner( void ); + protected: void ParseScriptedNPCInteractions( void ); void AddScriptedNPCInteraction( ScriptedNPCInteraction_t *pInteraction ); - const char *GetScriptedNPCInteractionSequence( ScriptedNPCInteraction_t *pInteraction, int iPhase ); + const char *GetScriptedNPCInteractionSequence( ScriptedNPCInteraction_t *pInteraction, int iPhase, bool bOtherNPC = false ); void StartRunningInteraction( CAI_BaseNPC *pOtherNPC, bool bActive ); void StartScriptedNPCInteraction( CAI_BaseNPC *pOtherNPC, ScriptedNPCInteraction_t *pInteraction, Vector vecOtherOrigin, QAngle angOtherAngles ); void CheckForScriptedNPCInteractions( void ); @@ -1320,17 +1335,16 @@ protected: #endif bool InteractionCouldStart( CAI_BaseNPC *pOtherNPC, ScriptedNPCInteraction_t *pInteraction, Vector &vecOrigin, QAngle &angAngles ); virtual bool CanRunAScriptedNPCInteraction( bool bForced = false ); - bool IsRunningDynamicInteraction( void ) { return (m_iInteractionState != NPCINT_NOT_RUNNING && (m_hCine != NULL)); } - bool IsActiveDynamicInteraction( void ) { return (m_iInteractionState == NPCINT_RUNNING_ACTIVE && (m_hCine != NULL)); } ScriptedNPCInteraction_t *GetRunningDynamicInteraction( void ) { return &(m_ScriptedInteractions[m_iInteractionPlaying]); } void SetInteractionCantDie( bool bCantDie ) { m_bCannotDieDuringInteraction = bCantDie; } bool HasInteractionCantDie( void ); + bool HasValidInteractionsOnCurrentEnemy( void ); + virtual bool CanStartDynamicInteractionDuringMelee() { return false; } void InputForceInteractionWithNPC( inputdata_t &inputdata ); void StartForcedInteraction( CAI_BaseNPC *pNPC, int iInteraction ); void CleanupForcedInteraction( void ); void CalculateForcedInteractionPosition( void ); - CAI_BaseNPC *GetInteractionPartner( void ); private: // Forced interactions @@ -2228,6 +2242,9 @@ public: static const char* GetActivityName (int actID); static void AddActivityToSR(const char *actName, int conID); +#ifdef MAPBASE + static int GetOrRegisterActivity( const char *actName ); +#endif static void AddEventToSR(const char *eventName, int conID); static const char* GetEventName (int actID);