diff --git a/.github/workflows/mapbase_build-base.yml b/.github/workflows/mapbase_build-base.yml index 43a4e3ca..fac52365 100644 --- a/.github/workflows/mapbase_build-base.yml +++ b/.github/workflows/mapbase_build-base.yml @@ -194,7 +194,9 @@ jobs: - uses: actions/checkout@v3 - name: Install GCC/G++ multilib - run: sudo apt-get install gcc-multilib g++-multilib + run: | + sudo apt-get update + sudo apt-get install gcc-multilib g++-multilib - name: Pick game if: inputs.project-group == 'game' || inputs.project-group == 'shaders' diff --git a/README b/README index ead59b89..d9d11024 100644 --- a/README +++ b/README @@ -144,6 +144,8 @@ Direct contributions: =-- https://github.com/mapbase-source/source-sdk-2013/pull/206 (Fix CScriptNetMsgHelper::WriteEntity()) =-- https://github.com/mapbase-source/source-sdk-2013/pull/213 (VScript HUD visibility control, optimizations for 3D skybox angles/fake worldportals) =-- https://github.com/mapbase-source/source-sdk-2013/pull/229 (VScript VGUI HUD viewport parenting, game_text and vgui_text_display VScript font fallback) +=-- https://github.com/mapbase-source/source-sdk-2013/pull/261 (Misc VScript additions) +=-- https://github.com/mapbase-source/source-sdk-2013/pull/279 (weapon_custom_scripted fixes) == Contributions from z33ky: =-- https://github.com/mapbase-source/source-sdk-2013/pull/21 (Various GCC/Linux compilation fixes) diff --git a/sp/src/game/client/c_baseanimating.cpp b/sp/src/game/client/c_baseanimating.cpp index 7afc4d22..edde6282 100644 --- a/sp/src/game/client/c_baseanimating.cpp +++ b/sp/src/game/client/c_baseanimating.cpp @@ -4042,6 +4042,92 @@ void C_BaseAnimating::FireEvent( const Vector& origin, const QAngle& angles, int } break; +#ifdef MAPBASE // From Alien Swarm SDK + case AE_CL_STOP_PARTICLE_EFFECT: + { + char token[256]; + char szParticleEffect[256]; + + // Get the particle effect name + const char *p = options; + p = nexttoken(token, p, ' ', sizeof(token)); + if ( token ) + { + Q_strncpy( szParticleEffect, token, sizeof(szParticleEffect) ); + } + + // Get the attachment point index + p = nexttoken(token, p, ' ', sizeof(token)); + bool bStopInstantly = ( token && !Q_stricmp( token, "instantly" ) ); + + ParticleProp()->StopParticlesNamed( szParticleEffect, bStopInstantly ); + } + break; + + case AE_CL_ADD_PARTICLE_EFFECT_CP: + { + int iControlPoint = 1; + int iAttachment = -1; + int iAttachType = PATTACH_ABSORIGIN_FOLLOW; + int iEffectIndex = -1; + char token[256]; + char szParticleEffect[256]; + + // Get the particle effect name + const char *p = options; + p = nexttoken(token, p, ' ', sizeof(token)); + if ( token ) + { + Q_strncpy( szParticleEffect, token, sizeof(szParticleEffect) ); + } + + // Get the control point number + p = nexttoken(token, p, ' ', sizeof(token)); + if ( token ) + { + iControlPoint = atoi( token ); + } + + // Get the attachment type + p = nexttoken(token, p, ' ', sizeof(token)); + if ( token ) + { + iAttachType = GetAttachTypeFromString( token ); + if ( iAttachType == -1 ) + { + Warning("Invalid attach type specified for particle effect anim event. Trying to spawn effect '%s' with attach type of '%s'\n", szParticleEffect, token ); + return; + } + } + + // Get the attachment point index + p = nexttoken(token, p, ' ', sizeof(token)); + if ( token ) + { + iAttachment = atoi(token); + + // See if we can find any attachment points matching the name + if ( token[0] != '0' && iAttachment == 0 ) + { + iAttachment = LookupAttachment( token ); + if ( iAttachment == -1 ) + { + Warning("Failed to find attachment point specified for particle effect anim event. Trying to spawn effect '%s' on attachment named '%s'\n", szParticleEffect, token ); + return; + } + } + } + iEffectIndex = ParticleProp()->FindEffect( szParticleEffect ); + if ( iEffectIndex == -1 ) + { + Warning("Failed to find specified particle effect. Trying to add CP to '%s' on attachment named '%s'\n", szParticleEffect, token ); + return; + } + ParticleProp()->AddControlPoint( iEffectIndex, iControlPoint, this, (ParticleAttachment_t)iAttachType, iAttachment ); + } + break; +#endif + case AE_CL_PLAYSOUND: { CLocalPlayerFilter filter; @@ -4291,6 +4377,22 @@ void C_BaseAnimating::FireEvent( const Vector& origin, const QAngle& angles, int } break; +#ifdef MAPBASE + case AE_VSCRIPT_RUN: + { + if (!RunScript( options )) + Warning( "%s failed to run AE_VSCRIPT_RUN on client with \"%s\"\n", GetDebugName(), options ); + } + break; + + case AE_VSCRIPT_RUN_FILE: + { + if (!RunScriptFile( options )) + Warning( "%s failed to run AE_VSCRIPT_RUN_FILE on client with \"%s\"\n", GetDebugName(), options ); + } + break; +#endif + default: break; } diff --git a/sp/src/game/client/mapbase/vscript_vgui.cpp b/sp/src/game/client/mapbase/vscript_vgui.cpp index 815679e0..805d3305 100644 --- a/sp/src/game/client/mapbase/vscript_vgui.cpp +++ b/sp/src/game/client/mapbase/vscript_vgui.cpp @@ -1477,6 +1477,11 @@ public: { ivgui()->AddTickSignal( this->GetVPanel(), i ); } + + void RemoveTickSignal() + { + ivgui()->RemoveTickSignal( this->GetVPanel() ); + } #if SCRIPT_VGUI_SIGNAL_INTERFACE void AddActionSignalTarget( HSCRIPT messageTarget ) { @@ -1867,6 +1872,7 @@ public: DEFINE_SCRIPTFUNC( MakeReadyForUse, "" )\ DEFINE_SCRIPTFUNC( GetName, "" )\ DEFINE_SCRIPTFUNC( AddTickSignal, "" )\ + DEFINE_SCRIPTFUNC( RemoveTickSignal, "" )\ \ DEFINE_SCRIPTFUNC( GetParent, "" )\ DEFINE_SCRIPTFUNC( SetParent, "" )\ 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..002f3f36 100644 --- a/sp/src/game/server/ai_basenpc.cpp +++ b/sp/src/game/server/ai_basenpc.cpp @@ -671,13 +671,27 @@ void CAI_BaseNPC::Ignite( float flFlameLifetime, bool bNPCOnly, float flSize, bo { BaseClass::Ignite( flFlameLifetime, bNPCOnly, flSize, bCalledByLevelDesigner ); +#ifdef MAPBASE + // Alyx's enemy ignited code from below can now be run on any NPC as long as + // it's our current enemy. + if ( GetEnemy() && GetEnemy()->IsNPC() ) + { + GetEnemy()->MyNPCPointer()->EnemyIgnited( this ); + } +#endif + #ifdef HL2_EPISODIC CBasePlayer *pPlayer = AI_GetSinglePlayer(); if ( pPlayer && pPlayer->IRelationType( this ) != D_LI ) { CNPC_Alyx *alyx = CNPC_Alyx::GetAlyx(); +#ifdef MAPBASE + // Alyx's code continues to run if Alyx was not this NPC's enemy. + if ( alyx && alyx != GetEnemy() ) +#else if ( alyx ) +#endif { alyx->EnemyIgnited( this ); } @@ -3026,6 +3040,10 @@ void CAI_BaseNPC::PopulatePoseParameters( void ) m_poseAim_Yaw = LookupPoseParameter( "aim_yaw" ); m_poseMove_Yaw = LookupPoseParameter( "move_yaw" ); +#ifdef MAPBASE + m_poseInteractionRelativeYaw = LookupPoseParameter( "interaction_relative_yaw" ); +#endif + BaseClass::PopulatePoseParameters(); } @@ -9526,6 +9544,12 @@ void CAI_BaseNPC::HandleAnimEvent( animevent_t *pEvent ) { m_hCine->FireScriptEvent( atoi( pEvent->options ) ); } +#ifdef MAPBASE + else if ( GetHintNode() ) + { + GetHintNode()->FireScriptEvent( atoi( pEvent->options ) ); + } +#endif else { // FIXME: look so see if it's playing a vcd and fire those instead @@ -12333,6 +12357,7 @@ BEGIN_ENT_SCRIPTDESC( CAI_BaseNPC, CBaseCombatCharacter, "The base class all NPC DEFINE_SCRIPTFUNC( IsCommandable, "Check if the NPC is commandable." ) DEFINE_SCRIPTFUNC( IsInPlayerSquad, "Check if the NPC is in the player's squad." ) + DEFINE_SCRIPTFUNC( IsMedic, "Returns true if this NPC is a medic." ) DEFINE_SCRIPTFUNC_NAMED( VScriptGetCine, "GetCine", "Get the NPC's currently running scripted sequence if it has one." ) DEFINE_SCRIPTFUNC( GetScriptState, "Get the NPC's current scripted sequence state." ) @@ -12443,7 +12468,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() @@ -13507,6 +13536,10 @@ bool CAI_BaseNPC::CineCleanup() } // Clear interaction partner, because we're not running a scripted sequence anymore +#ifdef MAPBASE + // We need the interaction partner for server ragdoll death cleanup, so don't clear if we're not alive + if (IsAlive()) +#endif m_hInteractionPartner = NULL; CleanupForcedInteraction(); } @@ -14847,32 +14880,66 @@ 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)) + { + const char *szTheirName = szName + 6; + sInteraction.bHasSeparateSequenceNames = true; + + if (!Q_strncmp(szTheirName, "entry_sequence", 14)) + sInteraction.sTheirPhases[SNPCINT_ENTRY].iszSequence = AllocPooledString(szValue); + else if (!Q_strncmp(szTheirName, "entry_activity", 14)) + sInteraction.sTheirPhases[SNPCINT_ENTRY].iActivity = GetOrRegisterActivity(szValue); + + else if (!Q_strncmp(szTheirName, "sequence", 8)) + sInteraction.sTheirPhases[SNPCINT_SEQUENCE].iszSequence = AllocPooledString(szValue); + else if (!Q_strncmp(szTheirName, "activity", 8)) + sInteraction.sTheirPhases[SNPCINT_SEQUENCE].iActivity = GetOrRegisterActivity(szValue); + + else if (!Q_strncmp(szTheirName, "exit_sequence", 13)) + sInteraction.sTheirPhases[SNPCINT_EXIT].iszSequence = AllocPooledString(szValue); + else if (!Q_strncmp(szTheirName, "exit_activity", 13)) + sInteraction.sTheirPhases[SNPCINT_EXIT].iActivity = GetOrRegisterActivity(szValue); + + // Add anything else to our miscellaneous criteria + else + { + szCriteria = UTIL_VarArgs("%s,%s:%s", szCriteria, szName, 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 +14966,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 +15202,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 +15312,37 @@ 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 ) + { + if (pOtherInteraction->flNextAttemptTime < pInteraction->flNextAttemptTime) + 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 +15374,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 ); @@ -15279,6 +15406,26 @@ void CAI_BaseNPC::StartScriptedNPCInteraction( CAI_BaseNPC *pOtherNPC, ScriptedN // Tell their sequence to keep their position relative to me pTheirSequence->SetupInteractionPosition( this, pInteraction->matDesiredLocalToWorld ); + +#ifdef MAPBASE + if ( !(pInteraction->iFlags & SCNPC_FLAG_TEST_OTHER_ANGLES) ) + { + // Set up interaction yaw pose if it exists + float flYaw = AngleDistance( angDesired.y, angOtherAngles.y ); + + int nInteractionPose = LookupPoseInteractionRelativeYaw(); + if (nInteractionPose > -1) + { + SetPoseParameter( nInteractionPose, flYaw ); + } + + nInteractionPose = pOtherNPC->LookupPoseInteractionRelativeYaw(); + if (nInteractionPose > -1) + { + pOtherNPC->SetPoseParameter( nInteractionPose, flYaw ); + } + } +#endif } // Spawn both sequences at once @@ -15367,7 +15514,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 +15703,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 +15726,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 +15895,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 @@ -15827,6 +16021,11 @@ bool CAI_BaseNPC::InteractionCouldStart( CAI_BaseNPC *pOtherNPC, ScriptedNPCInte { Msg(" %s distsqr: %0.2f (%0.2f %0.2f %0.2f), desired: <%0.2f (%0.2f %0.2f %0.2f)\n", GetDebugName(), flDistSqr, pOtherNPC->GetAbsOrigin().x, pOtherNPC->GetAbsOrigin().y, pOtherNPC->GetAbsOrigin().z, pInteraction->flDistSqr, vecOrigin.x, vecOrigin.y, vecOrigin.z ); +#ifdef MAPBASE + Vector vecForward, vecRight; + GetVectors( &vecForward, &vecRight, NULL ); + NDebugOverlay::Circle( vecOrigin + Vector(0,0,2), vecForward, vecRight, FastSqrt(pInteraction->flDistSqr), 255, 0, 0, 255, true, 0.1f ); +#endif } } } @@ -15839,6 +16038,11 @@ bool CAI_BaseNPC::InteractionCouldStart( CAI_BaseNPC *pOtherNPC, ScriptedNPCInte Msg(" %s is at: %0.2f %0.2f %0.2f\n", GetDebugName(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); Msg(" %s distsqr: %0.2f (%0.2f %0.2f %0.2f), desired: (%0.2f %0.2f %0.2f)\n", GetDebugName(), flDistSqr, pOtherNPC->GetAbsOrigin().x, pOtherNPC->GetAbsOrigin().y, pOtherNPC->GetAbsOrigin().z, vecOrigin.x, vecOrigin.y, vecOrigin.z ); +#ifdef MAPBASE + Vector vecForward, vecRight; + GetVectors( &vecForward, &vecRight, NULL ); + NDebugOverlay::Circle( vecOrigin + Vector( 0, 0, 2 ), vecForward, vecRight, FastSqrt( pInteraction->flDistSqr ), 255, 0, 0, 255, true, 0.1f ); +#endif if ( pOtherNPC ) { @@ -15855,14 +16059,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 ) { @@ -15870,6 +16088,13 @@ bool CAI_BaseNPC::InteractionCouldStart( CAI_BaseNPC *pOtherNPC, ScriptedNPCInte anglemod(angEnemyAngles.x), anglemod(angEnemyAngles.y), anglemod(angEnemyAngles.z), anglemod(angAngles.x), anglemod(angAngles.y), anglemod(angAngles.z) ); } } +#ifdef MAPBASE + else + { + // If we're not using angles, then use the NPC's current angles + angAngles = pOtherNPC->GetAbsAngles(); + } +#endif // TODO: Velocity check, if we're supposed to if ( pInteraction->iFlags & SCNPC_FLAG_TEST_OTHER_VELOCITY ) @@ -15949,6 +16174,7 @@ bool CAI_BaseNPC::InteractionCouldStart( CAI_BaseNPC *pOtherNPC, ScriptedNPCInte if ( bDebug ) { NDebugOverlay::Box( vecPos, GetHullMins(), GetHullMaxs(), 255,0,0, 100, 1.0 ); + NDebugOverlay::HorzArrow( GetAbsOrigin(), vecPos, 16.0f, 255, 0, 0, 255, true, 1.0f ); } return false; } @@ -15956,7 +16182,39 @@ bool CAI_BaseNPC::InteractionCouldStart( CAI_BaseNPC *pOtherNPC, ScriptedNPCInte { //NDebugOverlay::Box( vecPos, GetHullMins(), GetHullMaxs(), 0,255,0, 100, 1.0 ); - NDebugOverlay::Axis( vecPos, angAngles, 20, true, 10.0 ); + NDebugOverlay::Axis( vecPos, angAngles, 20, true, 1.0 ); + } + } + else + { + // Instead, make sure we fit into where the sequence movement ends at + const char *pszSequence = GetScriptedNPCInteractionSequence( pInteraction, SNPCINT_SEQUENCE ); + int nSeq = LookupSequence( pszSequence ); + if ( pszSequence && nSeq != -1 ) + { + Vector vecDeltaPos; + QAngle angDeltaAngles; + GetSequenceMovement( nSeq, 0.0f, 1.0f, vecDeltaPos, angDeltaAngles ); + if (!vecDeltaPos.IsZero()) + { + QAngle angInteraction = GetAbsAngles(); + angInteraction[YAW] = m_flInteractionYaw; + + Vector vecPos; + VectorRotate( vecDeltaPos, angInteraction, vecPos ); + vecPos += GetAbsOrigin(); + + AI_TraceHull( vecPos, vecPos, GetHullMins(), GetHullMaxs(), MASK_SOLID, &traceFilter, &tr); + if ( tr.fraction != 1.0 ) + { + if ( bDebug ) + { + NDebugOverlay::Box( vecPos, GetHullMins(), GetHullMaxs(), 255,0,0, 100, 1.0 ); + NDebugOverlay::HorzArrow( GetAbsOrigin(), vecPos, 16.0f, 255, 0, 0, 255, true, 1.0f ); + } + return false; + } + } } } #endif @@ -15980,6 +16238,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 - @@ -16213,6 +16490,21 @@ void CAI_BaseNPC::ModifyOrAppendEnemyCriteria( AI_CriteriaSet& set, CBaseEntity set.AppendCriteria( "enemyclass", g_pGameRules->AIClassText( pEnemy->Classify() ) ); // UTIL_VarArgs("%i", pEnemy->Classify()) set.AppendCriteria( "distancetoenemy", UTIL_VarArgs( "%f", EnemyDistance(pEnemy) ) ); set.AppendCriteria( "timesincecombat", "-1" ); + + CAI_BaseNPC *pNPC = pEnemy->MyNPCPointer(); + if (pNPC) + { + set.AppendCriteria("enemy_is_npc", "1"); + + set.AppendCriteria( "enemy_activity", CAI_BaseNPC::GetActivityName( pNPC->GetActivity() ) ); + set.AppendCriteria( "enemy_weapon", pNPC->GetActiveWeapon() ? pNPC->GetActiveWeapon()->GetClassname() : "0" ); + } + else + { + set.AppendCriteria("enemy_is_npc", "0"); + } + + pEnemy->AppendContextToCriteria( set, "enemy_" ); } else { diff --git a/sp/src/game/server/ai_basenpc.h b/sp/src/game/server/ai_basenpc.h index 878a6f81..dbc229e2 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(); @@ -838,6 +849,9 @@ protected: // pose parameters int m_poseAim_Pitch; int m_poseAim_Yaw; int m_poseMove_Yaw; +#ifdef MAPBASE + int m_poseInteractionRelativeYaw; +#endif virtual void PopulatePoseParameters( void ); public: @@ -845,6 +859,10 @@ public: // Return the stored pose parameter for "move_yaw" inline int LookupPoseMoveYaw() { return m_poseMove_Yaw; } + +#ifdef MAPBASE + inline int LookupPoseInteractionRelativeYaw() { return m_poseInteractionRelativeYaw; } +#endif //----------------------------------------------------- @@ -1304,10 +1322,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 +1342,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 @@ -1974,6 +1995,9 @@ public: //--------------------------------- virtual void Ignite( float flFlameLifetime, bool bNPCOnly = true, float flSize = 0.0f, bool bCalledByLevelDesigner = false ); +#ifdef MAPBASE + virtual void EnemyIgnited( CAI_BaseNPC *pVictim ) {} +#endif virtual bool PassesDamageFilter( const CTakeDamageInfo &info ); //--------------------------------- @@ -2228,6 +2252,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); diff --git a/sp/src/game/server/ai_basenpc_schedule.cpp b/sp/src/game/server/ai_basenpc_schedule.cpp index 61fefb3f..5f57b51d 100644 --- a/sp/src/game/server/ai_basenpc_schedule.cpp +++ b/sp/src/game/server/ai_basenpc_schedule.cpp @@ -1610,6 +1610,12 @@ void CAI_BaseNPC::StartTask( const Task_t *pTask ) // as this should only run with the NPC "receiving" the interaction ScriptedNPCInteraction_t *pInteraction = m_hForcedInteractionPartner->GetRunningDynamicInteraction(); + if ( !(pInteraction->iFlags & SCNPC_FLAG_TEST_OTHER_ANGLES) ) + { + TaskComplete(); + return; + } + // Get our target's origin Vector vecTarget = m_hForcedInteractionPartner->GetAbsOrigin(); @@ -1617,7 +1623,7 @@ void CAI_BaseNPC::StartTask( const Task_t *pTask ) float angInteractionAngle = pInteraction->angRelativeAngles.y; angInteractionAngle += 180.0f; - GetMotor()->SetIdealYaw( CalcIdealYaw( vecTarget ) + angInteractionAngle ); + GetMotor()->SetIdealYaw( AngleNormalize( CalcIdealYaw( vecTarget ) + angInteractionAngle ) ); if (FacingIdeal()) TaskComplete(); @@ -4113,6 +4119,15 @@ void CAI_BaseNPC::RunTask( const Task_t *pTask ) m_hCine->SynchronizeSequence( this ); } } + +#ifdef MAPBASE + if ( IsRunningDynamicInteraction() && m_poseInteractionRelativeYaw > -1 ) + { + // Animations in progress require pose parameters to be set every frame, so keep setting the interaction relative yaw pose. + // The random value is added to help it pass server transmit checks. + SetPoseParameter( m_poseInteractionRelativeYaw, GetPoseParameter( m_poseInteractionRelativeYaw ) + RandomFloat( -0.1f, 0.1f ) ); + } +#endif break; } diff --git a/sp/src/game/server/ai_expresserfollowup.cpp b/sp/src/game/server/ai_expresserfollowup.cpp index 65575709..38a83a3d 100644 --- a/sp/src/game/server/ai_expresserfollowup.cpp +++ b/sp/src/game/server/ai_expresserfollowup.cpp @@ -88,6 +88,15 @@ static void DispatchComeback( CAI_ExpresserWithFollowup *pExpress, CBaseEntity * // add in any provided contexts from the parameters onto the ones stored in the followup criteria.Merge( followup.followup_contexts ); +#ifdef MAPBASE + if (CAI_ExpresserSink *pSink = dynamic_cast(pRespondent)) + { + criteria.AppendCriteria( "dist_from_issuer", UTIL_VarArgs( "%f", (pRespondent->GetAbsOrigin() - pSpeaker->GetAbsOrigin()).Length() ) ); + g_ResponseQueueManager.GetQueue()->AppendFollowupCriteria( followup.followup_concept, criteria, pSink->GetSinkExpresser(), pSink, pRespondent, pSpeaker, kDRT_SPECIFIC ); + + pSink->Speak( followup.followup_concept, &criteria ); + } +#else // 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(pRespondent)) @@ -99,6 +108,7 @@ static void DispatchComeback( CAI_ExpresserWithFollowup *pExpress, CBaseEntity * { pActor->Speak( followup.followup_concept, &criteria ); } +#endif } #if 0 diff --git a/sp/src/game/server/ai_hint.cpp b/sp/src/game/server/ai_hint.cpp index e9844a01..10d9d728 100644 --- a/sp/src/game/server/ai_hint.cpp +++ b/sp/src/game/server/ai_hint.cpp @@ -902,6 +902,17 @@ BEGIN_DATADESC( CAI_Hint ) DEFINE_OUTPUT( m_OnNPCStartedUsing, "OnNPCStartedUsing" ), DEFINE_OUTPUT( m_OnNPCStoppedUsing, "OnNPCStoppedUsing" ), +#ifdef MAPBASE + DEFINE_OUTPUT( m_OnScriptEvent[0], "OnScriptEvent01" ), + DEFINE_OUTPUT( m_OnScriptEvent[1], "OnScriptEvent02" ), + DEFINE_OUTPUT( m_OnScriptEvent[2], "OnScriptEvent03" ), + DEFINE_OUTPUT( m_OnScriptEvent[3], "OnScriptEvent04" ), + DEFINE_OUTPUT( m_OnScriptEvent[4], "OnScriptEvent05" ), + DEFINE_OUTPUT( m_OnScriptEvent[5], "OnScriptEvent06" ), + DEFINE_OUTPUT( m_OnScriptEvent[6], "OnScriptEvent07" ), + DEFINE_OUTPUT( m_OnScriptEvent[7], "OnScriptEvent08" ), +#endif + END_DATADESC( ); #ifdef MAPBASE_VSCRIPT @@ -1705,6 +1716,19 @@ void CAI_Hint::NPCStoppedUsing( CAI_BaseNPC *pNPC ) m_OnNPCStoppedUsing.Set( pNPC, pNPC, this ); } +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_Hint::FireScriptEvent( int nEvent ) +{ + if ( ( nEvent >= 1 ) && ( nEvent <= 8 ) ) + { + m_OnScriptEvent[nEvent - 1].FireOutput( m_hHintOwner, this ); + } +} +#endif + CON_COMMAND(ai_dump_hints, "") { diff --git a/sp/src/game/server/ai_hint.h b/sp/src/game/server/ai_hint.h index fc23f9c3..b18be429 100644 --- a/sp/src/game/server/ai_hint.h +++ b/sp/src/game/server/ai_hint.h @@ -323,6 +323,9 @@ public: void FixupTargetNode(); void NPCStartedUsing( CAI_BaseNPC *pNPC ); void NPCStoppedUsing( CAI_BaseNPC *pNPC ); +#ifdef MAPBASE + void FireScriptEvent( int nEvent ); +#endif HintIgnoreFacing_t GetIgnoreFacing() const { return m_NodeData.fIgnoreFacing; } @@ -385,6 +388,10 @@ private: float m_nodeFOV; Vector m_vecForward; +#ifdef MAPBASE + COutputEvent m_OnScriptEvent[8]; +#endif + // The next hint in list of all hints friend class CAI_HintManager; diff --git a/sp/src/game/server/ai_playerally.cpp b/sp/src/game/server/ai_playerally.cpp index f7594feb..af969868 100644 --- a/sp/src/game/server/ai_playerally.cpp +++ b/sp/src/game/server/ai_playerally.cpp @@ -128,6 +128,12 @@ ConceptInfo_t g_ConceptInfos[] = // Passenger behaviour { TLK_PASSENGER_NEW_RADAR_CONTACT, SPEECH_IMPORTANT, -1, -1, -1, -1, -1, -1, AICF_DEFAULT, }, + +#ifdef MAPBASE + { TLK_TAKING_FIRE, SPEECH_IMPORTANT,-1, -1, -1, -1, -1, -1, AICF_DEFAULT, }, + { TLK_NEW_ENEMY, SPEECH_IMPORTANT,-1, -1, -1, -1, -1, -1, AICF_DEFAULT, }, + { TLK_COMBAT_IDLE, SPEECH_IMPORTANT,-1, -1, -1, -1, -1, -1, AICF_DEFAULT, }, +#endif }; //----------------------------------------------------------------------------- @@ -1259,6 +1265,38 @@ void CAI_PlayerAlly::OnKilledNPC( CBaseCombatCharacter *pKilled ) } } +#ifdef MAPBASE +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_PlayerAlly::OnEnemyRangeAttackedMe( CBaseEntity *pEnemy, const Vector &vecDir, const Vector &vecEnd ) +{ + BaseClass::OnEnemyRangeAttackedMe( pEnemy, vecDir, vecEnd ); + + if ( IRelationType( pEnemy ) <= D_FR ) + { + AI_CriteriaSet modifiers; + ModifyOrAppendEnemyCriteria( modifiers, pEnemy ); + + Vector vecEntDir = (pEnemy->EyePosition() - EyePosition()); + float flDot = DotProduct( vecEntDir.Normalized(), vecDir ); + modifiers.AppendCriteria( "shot_dot", CNumStr( flDot ) ); + + if (GetLastDamageTime() == gpGlobals->curtime) + modifiers.AppendCriteria( "missed", "0" ); + else + modifiers.AppendCriteria( "missed", "1" ); + + // Check if they're out of ammo + if ( pEnemy->IsCombatCharacter() && pEnemy->MyCombatCharacterPointer()->GetActiveWeapon() && pEnemy->MyCombatCharacterPointer()->GetActiveWeapon()->Clip1() <= 0 ) + modifiers.AppendCriteria( "last_attack", "1" ); + else + modifiers.AppendCriteria( "last_attack", "0" ); + + SpeakIfAllowed( TLK_TAKING_FIRE, modifiers ); + } +} +#endif + //----------------------------------------------------------------------------- void CAI_PlayerAlly::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) { @@ -1762,6 +1800,54 @@ bool CAI_PlayerAlly::IsAllowedToSpeak( AIConcept_t concept, bool bRespondingToPl return true; } +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: Specifically for player allies handling followup responses. +// Better-accounts for unknown concepts so that users are free in what they use. +//----------------------------------------------------------------------------- +bool CAI_PlayerAlly::IsAllowedToSpeakFollowup( AIConcept_t concept, CBaseEntity *pIssuer, bool bSpecific ) +{ + CAI_AllySpeechManager * pSpeechManager = GetAllySpeechManager(); + ConceptInfo_t * pInfo = pSpeechManager->GetConceptInfo( concept ); + ConceptCategory_t category = SPEECH_PRIORITY; // Must be SPEECH_PRIORITY to get around semaphore + + if ( !IsOkToSpeak( category, true ) ) + return false; + + // If this followup is specifically targeted towards us, speak if we're not already speaking + // If it's meant to be spoken by anyone, respect speech delay and semaphore + if ( bSpecific ) + { + if ( !GetExpresser()->CanSpeakAfterMyself() ) + return false; + } + else + { + if ( !GetExpresser()->CanSpeak() ) + return false; + + CAI_TimedSemaphore *pSemaphore = GetExpresser()->GetMySpeechSemaphore( this ); + if ( pSemaphore && !pSemaphore->IsAvailable( this ) ) + { + // Only if the semaphore holder isn't the one dispatching the followup + if ( pSemaphore->GetOwner() != pIssuer ) + return false; + } + } + + if ( !pSpeechManager->ConceptDelayExpired( concept ) ) + return false; + + if ( ( pInfo && pInfo->flags & AICF_SPEAK_ONCE ) && GetExpresser()->SpokeConcept( concept ) ) + return false; + + if ( !GetExpresser()->CanSpeakConcept( concept ) ) + return false; + + return true; +} +#endif + //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CAI_PlayerAlly::SpeakIfAllowed( AIConcept_t concept, const char *modifiers, bool bRespondingToPlayer, char *pszOutResponseChosen, size_t bufsize ) diff --git a/sp/src/game/server/ai_playerally.h b/sp/src/game/server/ai_playerally.h index 93448ec7..47ddf916 100644 --- a/sp/src/game/server/ai_playerally.h +++ b/sp/src/game/server/ai_playerally.h @@ -132,6 +132,13 @@ #define TLK_TGCATCHUP "TLK_TGCATCHUP" #define TLK_TGENDTOUR "TLK_TGENDTOUR" +#ifdef MAPBASE +// Additional concepts for companions in mods +#define TLK_TAKING_FIRE "TLK_TAKING_FIRE" // Someone fired at me (regardless of whether I was hit) +#define TLK_NEW_ENEMY "TLK_NEW_ENEMY" // A new enemy appeared while combat was already in progress +#define TLK_COMBAT_IDLE "TLK_COMBAT_IDLE" // Similar to TLK_ATTACKING, but specifically for when *not* currently attacking (e.g. when in cover or reloading) +#endif + //----------------------------------------------------------------------------- #define TALKRANGE_MIN 500.0 // don't talk to anyone farther away than this @@ -315,6 +322,10 @@ public: //--------------------------------- void OnKilledNPC( CBaseCombatCharacter *pKilled ); +#ifdef MAPBASE + void OnEnemyRangeAttackedMe( CBaseEntity *pEnemy, const Vector &vecDir, const Vector &vecEnd ); +#endif + //--------------------------------- // Damage handling //--------------------------------- @@ -392,6 +403,9 @@ public: bool ShouldSpeakRandom( AIConcept_t concept, int iChance ); bool IsAllowedToSpeak( AIConcept_t concept, bool bRespondingToPlayer = false ); +#ifdef MAPBASE + bool IsAllowedToSpeakFollowup( AIConcept_t concept, CBaseEntity *pIssuer, bool bSpecific ); +#endif virtual bool SpeakIfAllowed( AIConcept_t concept, const char *modifiers = NULL, bool bRespondingToPlayer = false, char *pszOutResponseChosen = NULL, size_t bufsize = 0 ); #ifdef MAPBASE virtual bool SpeakIfAllowed( AIConcept_t concept, AI_CriteriaSet& modifiers, bool bRespondingToPlayer = false, char *pszOutResponseChosen = NULL, size_t bufsize = 0 ); diff --git a/sp/src/game/server/ai_speech_new.h b/sp/src/game/server/ai_speech_new.h index c61f6f03..8290d471 100644 --- a/sp/src/game/server/ai_speech_new.h +++ b/sp/src/game/server/ai_speech_new.h @@ -126,6 +126,13 @@ public: virtual void OnSpokeConcept( AIConcept_t concept, AI_Response *response ) {}; virtual void OnStartSpeaking() {} virtual bool UseSemaphore() { return true; } + +#ifdef MAPBASE + // Works around issues with CAI_ExpresserHost<> class hierarchy + virtual CAI_Expresser *GetSinkExpresser() { return NULL; } + virtual bool IsAllowedToSpeakFollowup( AIConcept_t concept, CBaseEntity *pIssuer, bool bSpecific ) { return true; } + virtual bool Speak( AIConcept_t concept, AI_CriteriaSet *pCriteria, char *pszOutResponseChosen = NULL, size_t bufsize = 0, IRecipientFilter *filter = NULL ) { return false; } +#endif }; struct ConceptHistory_t @@ -244,9 +251,15 @@ public: static bool RunScriptResponse( CBaseEntity *pTarget, const char *response, AI_CriteriaSet *criteria, bool file ); #endif +#ifdef MAPBASE +public: +#else protected: +#endif CAI_TimedSemaphore *GetMySpeechSemaphore( CBaseEntity *pNpc ); +protected: + bool SpeakRawScene( const char *pszScene, float delay, AI_Response *response, IRecipientFilter *filter = NULL ); // This will create a fake .vcd/CChoreoScene to wrap the sound to be played #ifdef MAPBASE @@ -311,11 +324,15 @@ private: // template -class CAI_ExpresserHost : public BASE_NPC, protected CAI_ExpresserSink +class CAI_ExpresserHost : public BASE_NPC, public CAI_ExpresserSink { DECLARE_CLASS_NOFRIEND( CAI_ExpresserHost, BASE_NPC ); public: +#ifdef MAPBASE + CAI_Expresser *GetSinkExpresser() { return this->GetExpresser(); } +#endif + virtual void NoteSpeaking( float duration, float delay ); virtual bool Speak( AIConcept_t concept, const char *modifiers = NULL, char *pszOutResponseChosen = NULL, size_t bufsize = 0, IRecipientFilter *filter = NULL ); diff --git a/sp/src/game/server/ai_speechqueue.cpp b/sp/src/game/server/ai_speechqueue.cpp index 7e8bf055..58fccde6 100644 --- a/sp/src/game/server/ai_speechqueue.cpp +++ b/sp/src/game/server/ai_speechqueue.cpp @@ -11,6 +11,9 @@ #include "ai_baseactor.h" #include "ai_speech.h" //#include "flex_expresser.h" +#ifdef MAPBASE +#include "sceneentity.h" +#endif // memdbgon must be the last include file in a .cpp file!!! #include @@ -170,15 +173,25 @@ void CResponseQueue::RemoveExpresserHost(CBaseEntity *host) } } +#ifdef MAPBASE +/// Get the expresser for a base entity. +static CAI_Expresser *InferExpresserFromBaseEntity(CBaseEntity * RESTRICT pEnt, CAI_ExpresserSink **ppSink = NULL) +{ + if ( CAI_ExpresserSink *pSink = dynamic_cast(pEnt) ) + { + if (ppSink) + *ppSink = pSink; + return pSink->GetSinkExpresser(); + } + + return NULL; +} +#else /// 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) { -#ifdef MAPBASE - if ( CBasePlayer *pPlayer = ToBasePlayer(pEnt) ) -#else if ( CBaseMultiplayerPlayer *pPlayer = dynamic_cast(pEnt) ) -#endif { return pPlayer->GetExpresser(); } @@ -197,6 +210,7 @@ static CAI_Expresser *InferExpresserFromBaseEntity(CBaseEntity * RESTRICT pEnt) return NULL; } } +#endif void CResponseQueue::CDeferredResponse::Quash() @@ -205,6 +219,23 @@ void CResponseQueue::CDeferredResponse::Quash() m_fDispatchTime = 0; } +#ifdef MAPBASE +void CResponseQueue::AppendFollowupCriteria( AIConcept_t concept, AI_CriteriaSet &set, CAI_Expresser *pEx, + CAI_ExpresserSink *pSink, CBaseEntity *pTarget, CBaseEntity *pIssuer, DeferredResponseTarget_t nTargetType ) +{ + // Allows control over which followups interrupt speech routines + set.AppendCriteria( "followup_allowed_to_speak", (pSink->IsAllowedToSpeakFollowup( concept, pIssuer, nTargetType == kDRT_SPECIFIC )) ? "1" : "0" ); + + set.AppendCriteria( "followup_target_type", UTIL_VarArgs( "%i", (int)nTargetType ) ); + + // NOTE: This assumes any expresser entity derived from CBaseFlex is also derived from CBaseCombatCharacter + if (pTarget->IsCombatCharacter()) + set.AppendCriteria( "is_speaking", (pEx->IsSpeaking() || IsRunningScriptedSceneWithSpeechAndNotPaused( assert_cast(pTarget) )) ? "1" : "0" ); + else + set.AppendCriteria( "is_speaking", "0" ); +} +#endif + bool CResponseQueue::DispatchOneResponse(CDeferredResponse &response) { // find the target. @@ -272,9 +303,15 @@ bool CResponseQueue::DispatchOneResponse(CDeferredResponse &response) continue; // too far } +#ifdef MAPBASE + CAI_ExpresserSink *pSink = NULL; + pEx = InferExpresserFromBaseEntity( pTarget, &pSink ); +#else pEx = InferExpresserFromBaseEntity(pTarget); +#endif if ( !pEx || pTarget == pIssuer ) continue; + AI_CriteriaSet characterCriteria; pEx->GatherCriteria(&characterCriteria, response.m_concept, NULL); characterCriteria.Merge(&deferredCriteria); @@ -282,6 +319,11 @@ bool CResponseQueue::DispatchOneResponse(CDeferredResponse &response) { characterCriteria.AppendCriteria( "dist_from_issuer", UTIL_VarArgs( "%f", sqrt(distIssuerToTargetSq) ) ); } + +#ifdef MAPBASE + AppendFollowupCriteria( response.m_concept, characterCriteria, pEx, pSink, pTarget, pIssuer, kDRT_ALL ); +#endif + AI_Response prospectiveResponse; if ( pEx->FindResponse( prospectiveResponse, response.m_concept, &characterCriteria ) ) { @@ -304,14 +346,26 @@ bool CResponseQueue::DispatchOneResponse(CDeferredResponse &response) return false; // we're done right here. // Get the expresser for the target. +#ifdef MAPBASE + CAI_ExpresserSink *pSink = NULL; + pEx = InferExpresserFromBaseEntity( pTarget, &pSink ); +#else pEx = InferExpresserFromBaseEntity(pTarget); +#endif if (!pEx) return false; - AI_CriteriaSet characterCriteria; pEx->GatherCriteria(&characterCriteria, response.m_concept, NULL); characterCriteria.Merge(&deferredCriteria); +#ifdef MAPBASE + if ( pIssuer ) + { + characterCriteria.AppendCriteria( "dist_from_issuer", UTIL_VarArgs( "%f", (pTarget->GetAbsOrigin() - pIssuer->GetAbsOrigin()).Length() ) ); + } + + AppendFollowupCriteria( response.m_concept, characterCriteria, pEx, pSink, pTarget, pIssuer, kDRT_SPECIFIC ); +#endif pEx->Speak( response.m_concept, &characterCriteria ); return true; @@ -364,7 +418,12 @@ bool CResponseQueue::DispatchOneResponse_ThenANY( CDeferredResponse &response, A continue; // too far } +#ifdef MAPBASE + CAI_ExpresserSink *pSink = NULL; + pEx = InferExpresserFromBaseEntity( pTarget, &pSink ); +#else pEx = InferExpresserFromBaseEntity(pTarget); +#endif if ( !pEx ) continue; @@ -376,6 +435,11 @@ bool CResponseQueue::DispatchOneResponse_ThenANY( CDeferredResponse &response, A { characterCriteria.AppendCriteria( "dist_from_issuer", UTIL_VarArgs( "%f", sqrt(distIssuerToTargetSq) ) ); } + +#ifdef MAPBASE + AppendFollowupCriteria( response.m_concept, characterCriteria, pEx, pSink, pTarget, pIssuer, kDRT_ANY ); +#endif + AI_Response prospectiveResponse; #ifdef MAPBASE diff --git a/sp/src/game/server/ai_speechqueue.h b/sp/src/game/server/ai_speechqueue.h index 15101b70..87ab064f 100644 --- a/sp/src/game/server/ai_speechqueue.h +++ b/sp/src/game/server/ai_speechqueue.h @@ -116,6 +116,11 @@ public: inline int GetNumExpresserTargets() const; inline CBaseEntity *GetExpresserHost(int which) const; +#ifdef MAPBASE + void AppendFollowupCriteria( AIConcept_t concept, AI_CriteriaSet &set, CAI_Expresser *pEx, + CAI_ExpresserSink *pSink, CBaseEntity *pTarget, CBaseEntity *pIssuer, DeferredResponseTarget_t nTargetType ); +#endif + protected: /// Actually send off one response to a consumer /// Return true if dispatch succeeded diff --git a/sp/src/game/server/baseanimating.cpp b/sp/src/game/server/baseanimating.cpp index be4d6b73..0e226209 100644 --- a/sp/src/game/server/baseanimating.cpp +++ b/sp/src/game/server/baseanimating.cpp @@ -1333,7 +1333,7 @@ void CBaseAnimating::HandleAnimEvent( animevent_t *pEvent ) #ifdef MAPBASE else if ( pEvent->event == AE_NPC_RESPONSE ) { - if (!MyNPCPointer()->GetExpresser()->IsSpeaking()) + if (MyNPCPointer() && MyNPCPointer()->GetExpresser() && !MyNPCPointer()->GetExpresser()->IsSpeaking()) { DispatchResponse( pEvent->options ); } @@ -1344,6 +1344,18 @@ void CBaseAnimating::HandleAnimEvent( animevent_t *pEvent ) DispatchResponse( pEvent->options ); return; } + else if ( pEvent->event == AE_VSCRIPT_RUN ) + { + if (!RunScript( pEvent->options )) + Warning( "%s failed to run AE_VSCRIPT_RUN on server with \"%s\"\n", GetDebugName(), pEvent->options ); + return; + } + else if ( pEvent->event == AE_VSCRIPT_RUN_FILE ) + { + if (!RunScriptFile( pEvent->options )) + Warning( "%s failed to run AE_VSCRIPT_RUN_FILE on server with \"%s\"\n", GetDebugName(), pEvent->options ); + return; + } #endif else if ( pEvent->event == AE_RAGDOLL ) { diff --git a/sp/src/game/server/basecombatcharacter.h b/sp/src/game/server/basecombatcharacter.h index 22f07eb9..cdae243d 100644 --- a/sp/src/game/server/basecombatcharacter.h +++ b/sp/src/game/server/basecombatcharacter.h @@ -263,6 +263,10 @@ public: virtual bool CanBecomeServerRagdoll( void ) { return true; } +#ifdef MAPBASE + virtual void OnEnemyRangeAttackedMe( CBaseEntity *pEnemy, const Vector &vecDir, const Vector &vecEnd ) {} +#endif + // ----------------------- // Damage // ----------------------- diff --git a/sp/src/game/server/baseentity.cpp b/sp/src/game/server/baseentity.cpp index d8c4b63e..9df5cc09 100644 --- a/sp/src/game/server/baseentity.cpp +++ b/sp/src/game/server/baseentity.cpp @@ -10300,8 +10300,9 @@ bool CBaseEntity::ScriptAddOutput( const char *pszOutputName, const char *pszTar const char *CBaseEntity::ScriptGetKeyValue( const char *pszKeyName ) { static char szValue[128]; - GetKeyValue( pszKeyName, szValue, sizeof(szValue) ); - return szValue; + if ( GetKeyValue( pszKeyName, szValue, sizeof(szValue) ) ) + return szValue; + return NULL; } //----------------------------------------------------------------------------- diff --git a/sp/src/game/server/hl2/ai_behavior_functank.cpp b/sp/src/game/server/hl2/ai_behavior_functank.cpp index 89f79228..105bf6e2 100644 --- a/sp/src/game/server/hl2/ai_behavior_functank.cpp +++ b/sp/src/game/server/hl2/ai_behavior_functank.cpp @@ -190,6 +190,24 @@ int CAI_FuncTankBehavior::SelectSchedule() return SCHED_IDLE_STAND; } +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_FuncTankBehavior::ModifyOrAppendCriteria( AI_CriteriaSet &set ) +{ + BaseClass::ModifyOrAppendCriteria( set ); + +#ifdef MAPBASE + set.AppendCriteria( "ft_mounted", m_bMounted ? "1" : "0" ); + + if (m_hFuncTank) + { + set.AppendCriteria( "ft_classname", m_hFuncTank->GetClassname() ); + m_hFuncTank->AppendContextToCriteria( set, "ft_" ); + } +#endif +} + //----------------------------------------------------------------------------- // Purpose: // Input : activity - diff --git a/sp/src/game/server/hl2/ai_behavior_functank.h b/sp/src/game/server/hl2/ai_behavior_functank.h index 75d5b8df..54f91d1f 100644 --- a/sp/src/game/server/hl2/ai_behavior_functank.h +++ b/sp/src/game/server/hl2/ai_behavior_functank.h @@ -55,6 +55,8 @@ public: bool CanManTank( CFuncTank *pTank, bool bForced ); #endif + void ModifyOrAppendCriteria( AI_CriteriaSet &set ); + Activity NPC_TranslateActivity( Activity activity ); // Conditions: diff --git a/sp/src/game/server/hl2/npc_BaseZombie.cpp b/sp/src/game/server/hl2/npc_BaseZombie.cpp index fee51dd7..f751d09a 100644 --- a/sp/src/game/server/hl2/npc_BaseZombie.cpp +++ b/sp/src/game/server/hl2/npc_BaseZombie.cpp @@ -159,6 +159,10 @@ ConVar zombie_decaymax( "zombie_decaymax", "0.4" ); ConVar zombie_ambushdist( "zombie_ambushdist", "16000" ); +#ifdef MAPBASE +ConVar zombie_no_flinch_during_unique_anim( "zombie_no_flinch_during_unique_anim", "1", FCVAR_NONE, "Prevents zombies from flinching during actbusies and scripted sequences." ); +#endif + //========================================================= // For a couple of reasons, we keep a running count of how // many zombies in the world are angry at any given time. @@ -1745,7 +1749,11 @@ void CNPC_BaseZombie::HandleAnimEvent( animevent_t *pEvent ) dmgInfo.SetDamagePosition( vecHeadCrabPosition ); +#ifdef MAPBASE + ReleaseHeadcrab( vecHeadCrabPosition, vVelocity *iSpeed, true, false, true ); +#else ReleaseHeadcrab( EyePosition(), vVelocity * iSpeed, true, false, true ); +#endif GuessDamageForce( &dmgInfo, vVelocity, vecHeadCrabPosition, 0.5f ); TakeDamage( dmgInfo ); @@ -1927,6 +1935,31 @@ void CNPC_BaseZombie::OnScheduleChange( void ) } +//--------------------------------------------------------- +//--------------------------------------------------------- + +bool CNPC_BaseZombie::CanFlinch( void ) +{ + if (!BaseClass::CanFlinch()) + return false; + +#ifdef MAPBASE + if (zombie_no_flinch_during_unique_anim.GetBool()) + { + // Don't flinch if currently playing actbusy animation (navigating to or from one is fine) + if (m_ActBusyBehavior.IsInsideActBusy()) + return false; + + // Don't flinch if currently playing scripted sequence (navigating to or from one is fine) + if (m_NPCState == NPC_STATE_SCRIPT && (IsCurSchedule( SCHED_SCRIPTED_WAIT, false ) || IsCurSchedule( SCHED_SCRIPTED_FACE, false ))) + return false; + } +#endif + + return true; +} + + //--------------------------------------------------------- //--------------------------------------------------------- int CNPC_BaseZombie::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode ) @@ -2435,6 +2468,20 @@ void CNPC_BaseZombie::RemoveHead( void ) } +//--------------------------------------------------------- +//--------------------------------------------------------- +void CNPC_BaseZombie::SetModel( const char *szModelName ) +{ +#ifdef MAPBASE + // Zombies setting the same model again is a problem when they should maintain their current sequence (e.g. during dynamic interactions) + if ( IsRunningDynamicInteraction() && GetModelIndex() != 0 && FStrEq( szModelName, STRING(GetModelName()) ) ) + return; +#endif + + BaseClass::SetModel( szModelName ); +} + + bool CNPC_BaseZombie::ShouldPlayFootstepMoan( void ) { if( random->RandomInt( 1, zombie_stepfreq.GetInt() * s_iAngryZombies ) == 1 ) @@ -2450,9 +2497,15 @@ bool CNPC_BaseZombie::ShouldPlayFootstepMoan( void ) #define CRAB_HULL_EXPAND 1.1f //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- -bool CNPC_BaseZombie::HeadcrabFits( CBaseAnimating *pCrab ) +bool CNPC_BaseZombie::HeadcrabFits( CBaseAnimating *pCrab, const Vector *vecOrigin ) { - Vector vecSpawnLoc = pCrab->GetAbsOrigin(); + Vector vecSpawnLoc; +#ifdef MAPBASE + if (vecOrigin) + vecSpawnLoc = *vecOrigin; + else +#endif + vecSpawnLoc = pCrab->GetAbsOrigin(); CTraceFilterSimpleList traceFilter( COLLISION_GROUP_NONE ); traceFilter.AddEntityToIgnore( pCrab ); @@ -2535,7 +2588,12 @@ void CNPC_BaseZombie::ReleaseHeadcrab( const Vector &vecOrigin, const Vector &ve SetHeadcrabSpawnLocation( iCrabAttachment, pAnimatingGib ); } +#ifdef MAPBASE + // Server ragdolls don't have a valid origin on spawn, so we have to use the origin originally passed + if( !HeadcrabFits( pAnimatingGib, m_bForceServerRagdoll ? &vecOrigin : NULL ) ) +#else if( !HeadcrabFits(pAnimatingGib) ) +#endif { UTIL_Remove(pGib); return; @@ -2552,11 +2610,20 @@ void CNPC_BaseZombie::ReleaseHeadcrab( const Vector &vecOrigin, const Vector &ve if( UTIL_ShouldShowBlood(BLOOD_COLOR_YELLOW) ) { - UTIL_BloodImpact( pGib->WorldSpaceCenter(), Vector(0,0,1), BLOOD_COLOR_YELLOW, 1 ); + Vector vecGibCenter; +#ifdef MAPBASE + // Server ragdolls don't have a valid origin on spawn, so we have to use the origin originally passed + if (m_bForceServerRagdoll) + vecGibCenter = vecOrigin; + else +#endif + vecGibCenter = pGib->WorldSpaceCenter(); + + UTIL_BloodImpact( vecGibCenter, Vector(0,0,1), BLOOD_COLOR_YELLOW, 1 ); for ( int i = 0 ; i < 3 ; i++ ) { - Vector vecSpot = pGib->WorldSpaceCenter(); + Vector vecSpot = vecGibCenter; vecSpot.x += random->RandomFloat( -8, 8 ); vecSpot.y += random->RandomFloat( -8, 8 ); @@ -2590,6 +2657,9 @@ void CNPC_BaseZombie::ReleaseHeadcrab( const Vector &vecOrigin, const Vector &ve // Inherit some misc. properties pCrab->m_bForceServerRagdoll = m_bForceServerRagdoll; pCrab->m_iViewHideFlags = m_iViewHideFlags; + + // Add response context for companion response (more reliable than checking for post-death zombie entity) + pCrab->AddContext( "from_zombie", "1", 2.0f ); #endif // make me the crab's owner to avoid collision issues diff --git a/sp/src/game/server/hl2/npc_BaseZombie.h b/sp/src/game/server/hl2/npc_BaseZombie.h index 743186de..fb17f037 100644 --- a/sp/src/game/server/hl2/npc_BaseZombie.h +++ b/sp/src/game/server/hl2/npc_BaseZombie.h @@ -151,6 +151,8 @@ public: int OnTakeDamage_Alive( const CTakeDamageInfo &info ); virtual float GetReactionDelay( CBaseEntity *pEnemy ) { return 0.0; } + bool CanFlinch( void ); + virtual int SelectSchedule ( void ); virtual int SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode ); virtual void BuildScheduleTestBits( void ); @@ -186,9 +188,10 @@ public: // Headcrab releasing/breaking apart void RemoveHead( void ); virtual void SetZombieModel( void ) { }; + virtual void SetModel( const char *szModelName ); virtual void BecomeTorso( const Vector &vecTorsoForce, const Vector &vecLegsForce ); virtual bool CanBecomeLiveTorso() { return false; } - virtual bool HeadcrabFits( CBaseAnimating *pCrab ); + virtual bool HeadcrabFits( CBaseAnimating *pCrab, const Vector *vecOrigin = NULL ); void ReleaseHeadcrab( const Vector &vecOrigin, const Vector &vecVelocity, bool fRemoveHead, bool fRagdollBody, bool fRagdollCrab = false ); void SetHeadcrabSpawnLocation( int iCrabAttachment, CBaseAnimating *pCrab ); diff --git a/sp/src/game/server/hl2/npc_alyx_episodic.cpp b/sp/src/game/server/hl2/npc_alyx_episodic.cpp index ee8b197c..dd5a35b0 100644 --- a/sp/src/game/server/hl2/npc_alyx_episodic.cpp +++ b/sp/src/game/server/hl2/npc_alyx_episodic.cpp @@ -1092,10 +1092,14 @@ void CNPC_Alyx::Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo & //----------------------------------------------------------------------------- void CNPC_Alyx::EnemyIgnited( CAI_BaseNPC *pVictim ) { +#ifdef MAPBASE + BaseClass::EnemyIgnited( pVictim ); +#else if ( FVisible( pVictim ) ) { SpeakIfAllowed( TLK_ENEMY_BURNING ); } +#endif } //----------------------------------------------------------------------------- @@ -1252,6 +1256,7 @@ void CNPC_Alyx::DoCustomSpeechAI( void ) CBasePlayer *pPlayer = AI_GetSinglePlayer(); +#ifndef MAPBASE // Ported to CNPC_PlayerCompanion if ( HasCondition(COND_NEW_ENEMY) && GetEnemy() ) { if ( GetEnemy()->Classify() == CLASS_HEADCRAB ) @@ -1278,6 +1283,7 @@ void CNPC_Alyx::DoCustomSpeechAI( void ) } } } +#endif // Darkness mode speech ClearCondition( COND_ALYX_IN_DARK ); @@ -1917,6 +1923,7 @@ int CNPC_Alyx::SelectSchedule( void ) //----------------------------------------------------------------------------- int CNPC_Alyx::SelectScheduleDanger( void ) { +#ifndef MAPBASE if( HasCondition( COND_HEAR_DANGER ) ) { CSound *pSound; @@ -1929,6 +1936,7 @@ int CNPC_Alyx::SelectScheduleDanger( void ) SpeakIfAllowed( TLK_DANGER_ZOMBINE_GRENADE ); } } +#endif return BaseClass::SelectScheduleDanger(); } diff --git a/sp/src/game/server/hl2/npc_citizen17.cpp b/sp/src/game/server/hl2/npc_citizen17.cpp index be80179a..02baaf07 100644 --- a/sp/src/game/server/hl2/npc_citizen17.cpp +++ b/sp/src/game/server/hl2/npc_citizen17.cpp @@ -412,7 +412,6 @@ ScriptHook_t CNPC_Citizen::g_Hook_SelectModel; BEGIN_ENT_SCRIPTDESC( CNPC_Citizen, CAI_BaseActor, "npc_citizen from Half-Life 2" ) - DEFINE_SCRIPTFUNC( IsMedic, "Returns true if this citizen is a medic." ) DEFINE_SCRIPTFUNC( IsAmmoResupplier, "Returns true if this citizen is an ammo resupplier." ) DEFINE_SCRIPTFUNC( CanHeal, "Returns true if this citizen is a medic or ammo resupplier currently able to heal/give ammo." ) diff --git a/sp/src/game/server/hl2/npc_combine.cpp b/sp/src/game/server/hl2/npc_combine.cpp index 73bac242..c2cac902 100644 --- a/sp/src/game/server/hl2/npc_combine.cpp +++ b/sp/src/game/server/hl2/npc_combine.cpp @@ -47,6 +47,8 @@ ConVar npc_combine_protected_run( "npc_combine_protected_run", "0", FCVAR_NONE, ConVar npc_combine_altfire_not_allies_only( "npc_combine_altfire_not_allies_only", "1", FCVAR_NONE, "Mapbase: Elites are normally only allowed to fire their alt-fire attack at the player and the player's allies; This allows elites to alt-fire at other enemies too." ); ConVar npc_combine_new_cover_behavior( "npc_combine_new_cover_behavior", "1", FCVAR_NONE, "Mapbase: Toggles small patches for parts of npc_combine AI related to soldiers failing to take cover. These patches are minimal and only change cases where npc_combine would otherwise look at an enemy without shooting or run up to the player to melee attack when they don't have to. Consult the Mapbase wiki for more information." ); + +ConVar npc_combine_fixed_shootpos( "npc_combine_fixed_shootpos", "0", FCVAR_NONE, "Mapbase: Toggles fixed Combine soldier shoot position." ); #endif #define COMBINE_SKIN_DEFAULT 0 @@ -2959,6 +2961,28 @@ Vector CNPC_Combine::Weapon_ShootPosition( ) // FIXME: rename this "estimated" since it's not based on animation // FIXME: the orientation won't be correct when testing from arbitary positions for arbitary angles +#ifdef MAPBASE + // HACKHACK: This weapon shoot position code does not work properly when in close range, causing the aim + // to drift to the left as the enemy gets closer to it. + // This problem is usually bearable for regular combat, but it causes dynamic interaction yaw to be offset + // as well, preventing most from ever being triggered. + // Ideally, this should be fixed from the root cause, but due to the sensitivity of such a change, this is + // currently being tied to a cvar which is off by default. + // + // If the cvar is disabled but the soldier has valid interactions on its current enemy, then a separate hack + // will still attempt to correct the drift as the enemy gets closer. + if ( npc_combine_fixed_shootpos.GetBool() ) + { + right *= 0.0f; + } + else if ( HasValidInteractionsOnCurrentEnemy() ) + { + float flDistSqr = GetEnemy()->WorldSpaceCenter().DistToSqr( WorldSpaceCenter() ); + if (flDistSqr < Square( 128.0f )) + right *= (flDistSqr / Square( 128.0f )); + } +#endif + if ( bStanding ) { if( HasShotgun() ) diff --git a/sp/src/game/server/hl2/npc_playercompanion.cpp b/sp/src/game/server/hl2/npc_playercompanion.cpp index 68568a72..660cb218 100644 --- a/sp/src/game/server/hl2/npc_playercompanion.cpp +++ b/sp/src/game/server/hl2/npc_playercompanion.cpp @@ -35,6 +35,8 @@ #include "mapbase/GlobalStrings.h" #include "world.h" #include "vehicle_base.h" +#include "npc_headcrab.h" +#include "npc_BaseZombie.h" #endif ConVar ai_debug_readiness("ai_debug_readiness", "0" ); @@ -640,6 +642,55 @@ void CNPC_PlayerCompanion::DoCustomSpeechAI( void ) { SpeakIfAllowed( TLK_PLDEAD ); } + +#ifdef MAPBASE + // Unique new enemy concepts ported from Alyx + // The casts have been changed to dynamic_cast due to the risk of non-CBaseHeadcrab/CNPC_BaseZombie enemies using those classes + if ( HasCondition(COND_NEW_ENEMY) && GetEnemy() ) + { + int nClass = GetEnemy()->Classify(); + if ( nClass == CLASS_HEADCRAB ) + { + CBaseHeadcrab *pHC = dynamic_cast(GetEnemy()); + if ( pHC ) + { + // If we see a headcrab for the first time as he's jumping at me, freak out! + if ( ( GetEnemy()->GetEnemy() == this ) && pHC->IsJumping() && gpGlobals->curtime - GetEnemies()->FirstTimeSeen(GetEnemy()) < 0.5 ) + { + SpeakIfAllowed( "TLK_SPOTTED_INCOMING_HEADCRAB" ); + } + else + { + // If we see a headcrab leaving a zombie that just died, mention it + // (Note that this is now a response context since some death types remove the zombie instantly) + int nContext = pHC->FindContextByName( "from_zombie" ); + if ( nContext > -1 && !ContextExpired( nContext ) ) // pHC->GetOwnerEntity() && ( pHC->GetOwnerEntity()->Classify() == CLASS_ZOMBIE ) && !pHC->GetOwnerEntity()->IsAlive() + { + SpeakIfAllowed( "TLK_SPOTTED_HEADCRAB_LEAVING_ZOMBIE" ); + } + } + } + } + else if ( nClass == CLASS_ZOMBIE ) + { + CNPC_BaseZombie *pZombie = dynamic_cast(GetEnemy()); + // If we see a zombie getting up, mention it + if ( pZombie && pZombie->IsGettingUp() ) + { + SpeakIfAllowed( "TLK_SPOTTED_ZOMBIE_WAKEUP" ); + } + } + + if ( gpGlobals->curtime - GetEnemies()->TimeAtFirstHand( GetEnemy() ) <= 1.0f && nClass != CLASS_BULLSEYE ) + { + // New concept which did not originate from Alyx, but is in the same category as the above concepts. + // This is meant to be used when a new enemy enters the arena while combat is already in progress. + // (Note that this can still trigger when combat begins, but unlike TLK_STARTCOMBAT, it has no delay + // between combat engagements.) + SpeakIfAllowed( TLK_NEW_ENEMY ); + } + } +#endif } //----------------------------------------------------------------------------- @@ -910,8 +961,21 @@ int CNPC_PlayerCompanion::SelectScheduleDanger() if ( pSound && (pSound->m_iType & SOUND_DANGER) ) { +#ifdef MAPBASE + if ( pSound->SoundChannel() == SOUNDENT_CHANNEL_ZOMBINE_GRENADE ) + { + SetSpeechTarget( pSound->m_hOwner ); + SpeakIfAllowed( TLK_DANGER_ZOMBINE_GRENADE ); + } + else if (!(pSound->SoundContext() & (SOUND_CONTEXT_MORTAR | SOUND_CONTEXT_FROM_SNIPER)) || IsOkToCombatSpeak()) + { + SetSpeechTarget( pSound->m_hOwner ); + SpeakIfAllowed( TLK_DANGER ); + } +#else if ( !(pSound->SoundContext() & (SOUND_CONTEXT_MORTAR|SOUND_CONTEXT_FROM_SNIPER)) || IsOkToCombatSpeak() ) SpeakIfAllowed( TLK_DANGER ); +#endif if ( HasCondition( COND_PC_SAFE_FROM_MORTAR ) ) { @@ -4309,6 +4373,20 @@ void CNPC_PlayerCompanion::Event_KilledOther( CBaseEntity *pVictim, const CTakeD } } +//----------------------------------------------------------------------------- +// Purpose: Called by enemy NPC's when they are ignited +// Input : pVictim - entity that was ignited +//----------------------------------------------------------------------------- +void CNPC_PlayerCompanion::EnemyIgnited( CAI_BaseNPC *pVictim ) +{ + BaseClass::EnemyIgnited( pVictim ); + + if ( FVisible( pVictim ) ) + { + SpeakIfAllowed( TLK_ENEMY_BURNING ); + } +} + //----------------------------------------------------------------------------- // Purpose: Handles custom combat speech stuff ported from Alyx. //----------------------------------------------------------------------------- @@ -4376,6 +4454,21 @@ void CNPC_PlayerCompanion::DoCustomCombatAI( void ) { SpeakIfAllowed( TLK_MANY_ENEMIES ); } + + // If we're not currently attacking or vulnerable, try speaking + else if ( gpGlobals->curtime - GetLastAttackTime() > 1.0f && (!HasCondition( COND_SEE_ENEMY ) || IsCurSchedule( SCHED_RELOAD ) || IsCurSchedule( SCHED_HIDE_AND_RELOAD )) ) + { + int chance = ( IsMoving() ) ? 20 : 3; + if ( ShouldSpeakRandom( TLK_COMBAT_IDLE, chance ) ) + { + AI_CriteriaSet modifiers; + + modifiers.AppendCriteria( "in_cover", HasMemory( bits_MEMORY_INCOVER ) ? "1" : "0" ); + modifiers.AppendCriteria( "lastseenenemy", UTIL_VarArgs( "%f", gpGlobals->curtime - GetEnemyLastTimeSeen() ) ); + + SpeakIfAllowed( TLK_COMBAT_IDLE, modifiers ); + } + } } #endif diff --git a/sp/src/game/server/hl2/npc_playercompanion.h b/sp/src/game/server/hl2/npc_playercompanion.h index 8dcf1aa1..e0f6769e 100644 --- a/sp/src/game/server/hl2/npc_playercompanion.h +++ b/sp/src/game/server/hl2/npc_playercompanion.h @@ -242,6 +242,7 @@ public: virtual void Event_Killed( const CTakeDamageInfo &info ); virtual void Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ); + virtual void EnemyIgnited( CAI_BaseNPC *pVictim ); virtual void DoCustomCombatAI( void ); #endif diff --git a/sp/src/game/server/hl2/weapon_pistol.cpp b/sp/src/game/server/hl2/weapon_pistol.cpp index 9819f748..8a49f366 100644 --- a/sp/src/game/server/hl2/weapon_pistol.cpp +++ b/sp/src/game/server/hl2/weapon_pistol.cpp @@ -526,6 +526,10 @@ bool CWeaponPistol::Reload( void ) return fRet; } +#ifdef MAPBASE +ConVar weapon_pistol_upwards_viewkick( "weapon_pistol_upwards_viewkick", "0" ); +#endif + //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- @@ -538,7 +542,11 @@ void CWeaponPistol::AddViewKick( void ) QAngle viewPunch; +#ifdef MAPBASE + viewPunch.x = weapon_pistol_upwards_viewkick.GetBool() ? random->RandomFloat( -0.5f, -0.25f ) : random->RandomFloat( 0.25f, 0.5f ); +#else viewPunch.x = random->RandomFloat( 0.25f, 0.5f ); +#endif viewPunch.y = random->RandomFloat( -.6f, .6f ); viewPunch.z = 0.0f; diff --git a/sp/src/game/server/physics_prop_ragdoll.cpp b/sp/src/game/server/physics_prop_ragdoll.cpp index 93efddc7..cc788fc6 100644 --- a/sp/src/game/server/physics_prop_ragdoll.cpp +++ b/sp/src/game/server/physics_prop_ragdoll.cpp @@ -1557,6 +1557,16 @@ CBaseEntity *CreateServerRagdoll( CBaseAnimating *pAnimating, int forceBone, con pRagdoll->CollisionProp()->SetCollisionBounds( mins, maxs ); #ifdef MAPBASE + // If this was a NPC running a dynamic interaction, disable collisions with the interaction partner + if (pAnimating->IsNPC() /*&& pAnimating->MyNPCPointer()->IsRunningDynamicInteraction()*/) + { + CAI_BaseNPC *pNPC = pAnimating->MyNPCPointer(); + if (pNPC->GetInteractionPartner() && pNPC->GetInteractionPartner()->VPhysicsGetObject()) + { + PhysDisableEntityCollisions( pRagdoll, pNPC->GetInteractionPartner() ); + } + } + variant_t variant; variant.SetEntity(pRagdoll); pAnimating->FireNamedOutput("OnServerRagdoll", variant, pRagdoll, pAnimating); diff --git a/sp/src/game/server/player.cpp b/sp/src/game/server/player.cpp index f8bf33a2..28c767c5 100644 --- a/sp/src/game/server/player.cpp +++ b/sp/src/game/server/player.cpp @@ -7734,6 +7734,93 @@ void CBasePlayer::ResetAutoaim( void ) m_fOnTarget = false; } +#ifdef MAPBASE +ConVar player_debug_probable_aim_target( "player_debug_probable_aim_target", "0", FCVAR_CHEAT, "" ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseEntity *CBasePlayer::GetProbableAimTarget( const Vector &vecSrc, const Vector &vecDir ) +{ + trace_t tr; + CBaseEntity *pIgnore = NULL; + if (IsInAVehicle()) + pIgnore = GetVehicleEntity(); + + CTraceFilterSkipTwoEntities traceFilter( this, pIgnore, COLLISION_GROUP_NONE ); + + // Based on dot product and distance + // If we aim directly at something, only return it if there's not a larger entity slightly off-center + // Should be weighted based on whether an entity is a NPC, etc. + CBaseEntity *pBestEnt = NULL; + float flBestWeight = 0.0f; + for (CBaseEntity *pEntity = UTIL_EntitiesInPVS( this, NULL ); pEntity; pEntity = UTIL_EntitiesInPVS( this, pEntity )) + { + // Combat characters can be unviewable if they just died + if (!pEntity->IsViewable() && !pEntity->IsCombatCharacter()) + continue; + + if (pEntity == this || pEntity->GetMoveParent() == this || pEntity == GetVehicleEntity()) + continue; + + Vector vecEntDir = (pEntity->EyePosition() - vecSrc); + float flDot = DotProduct( vecEntDir.Normalized(), vecDir); + + if (flDot < m_flFieldOfView) + continue; + + // Make sure we can see it + UTIL_TraceLine( vecSrc, pEntity->EyePosition(), MASK_SHOT, &traceFilter, &tr ); + if (tr.m_pEnt != pEntity) + { + if (pEntity->IsCombatCharacter()) + { + // Trace between centers as well just in case our eyes are blocked + UTIL_TraceLine( WorldSpaceCenter(), pEntity->WorldSpaceCenter(), MASK_SHOT, &traceFilter, &tr ); + if (tr.m_pEnt != pEntity) + continue; + } + else + continue; + } + + float flWeight = flDot - (vecEntDir.LengthSqr() / Square( 2048.0f )); + + if (pEntity->IsCombatCharacter()) + { + // Hostile NPCs are more likely targets + if (IRelationType( pEntity ) <= D_FR) + flWeight += 0.5f; + } + else if (pEntity->GetFlags() & FL_AIMTARGET) + { + // FL_AIMTARGET is often used for props like explosive barrels + flWeight += 0.25f; + } + + if (player_debug_probable_aim_target.GetBool()) + { + float flWeightClamped = 1.0f - RemapValClamped( flWeight, -2.0f, 2.0f, 0.0f, 1.0f ); + pEntity->EntityText( 0, UTIL_VarArgs( "%f", flWeight ), 2.0f, flWeightClamped * 255.0f, 255.0f, flWeightClamped * 255.0f, 255 ); + } + + if (flWeight > flBestWeight) + { + pBestEnt = pEntity; + flBestWeight = flWeight; + } + } + + if (player_debug_probable_aim_target.GetBool()) + { + Msg( "Best probable aim target is %s\n", pBestEnt->GetDebugName() ); + NDebugOverlay::EntityBounds( pBestEnt, 255, 100, 0, 0, 2.0f ); + } + + return pBestEnt; +} +#endif + // ========================================================================== // > Weapon stuff // ========================================================================== diff --git a/sp/src/game/server/player.h b/sp/src/game/server/player.h index 0bfba8fb..72a1e38e 100644 --- a/sp/src/game/server/player.h +++ b/sp/src/game/server/player.h @@ -608,6 +608,11 @@ public: virtual bool ShouldAutoaim( void ); void SetTargetInfo( Vector &vecSrc, float flDist ); +#ifdef MAPBASE + // Tries to figure out what the player is trying to aim at + CBaseEntity *GetProbableAimTarget( const Vector &vecSrc, const Vector &vecDir ); +#endif + void SetViewEntity( CBaseEntity *pEntity ); CBaseEntity *GetViewEntity( void ) { return m_hViewEntity; } diff --git a/sp/src/game/server/scripted.cpp b/sp/src/game/server/scripted.cpp index 37b45844..f02a1dcd 100644 --- a/sp/src/game/server/scripted.cpp +++ b/sp/src/game/server/scripted.cpp @@ -1396,11 +1396,31 @@ void CAI_ScriptedSequence::ModifyScriptedAutoMovement( Vector *vecNewPos ) } } + VMatrix matInteractionPosition = m_matInteractionPosition; + +#ifdef MAPBASE + // Account for our own sequence movement + pAnimating = m_hTargetEnt->GetBaseAnimating(); + if (pAnimating) + { + Vector vecDeltaPos; + QAngle angDeltaAngles; + + pAnimating->GetSequenceMovement( pAnimating->GetSequence(), 0.0f, pAnimating->GetCycle(), vecDeltaPos, angDeltaAngles ); + if (!vecDeltaPos.IsZero()) + { + VMatrix matLocalMovement; + matLocalMovement.SetupMatrixOrgAngles( vecDeltaPos, angDeltaAngles ); + MatrixMultiply( m_matInteractionPosition, matLocalMovement, matInteractionPosition ); + } + } +#endif + // We've been asked to maintain a specific position relative to the other NPC // we're interacting with. Lerp towards the relative position. VMatrix matMeToWorld, matLocalToWorld; matMeToWorld.SetupMatrixOrgAngles( vecRelativeOrigin, angRelativeAngles ); - MatrixMultiply( matMeToWorld, m_matInteractionPosition, matLocalToWorld ); + MatrixMultiply( matMeToWorld, matInteractionPosition, matLocalToWorld ); // Get the desired NPC position in worldspace Vector vecOrigin; diff --git a/sp/src/game/shared/baseentity_shared.cpp b/sp/src/game/shared/baseentity_shared.cpp index e5588287..b2cd6bdc 100644 --- a/sp/src/game/shared/baseentity_shared.cpp +++ b/sp/src/game/shared/baseentity_shared.cpp @@ -83,6 +83,10 @@ ConVar ai_shot_bias_min( "ai_shot_bias_min", "-1.0", FCVAR_REPLICATED ); ConVar ai_shot_bias_max( "ai_shot_bias_max", "1.0", FCVAR_REPLICATED ); ConVar ai_debug_shoot_positions( "ai_debug_shoot_positions", "0", FCVAR_REPLICATED | FCVAR_CHEAT ); +#if defined(MAPBASE) && defined(GAME_DLL) +ConVar ai_shot_notify_targets( "ai_shot_notify_targets", "0", FCVAR_NONE, "Allows fired bullets to notify the NPCs and players they are targeting, regardless of whether they hit them or not. Can be used for custom AI and speech." ); +#endif + // Utility func to throttle rate at which the "reasonable position" spew goes out static double s_LastEntityReasonableEmitTime; bool CheckEmitReasonablePhysicsSpew() @@ -2081,6 +2085,25 @@ void CBaseEntity::FireBullets( const FireBulletsInfo_t &info ) CTakeDamageInfo dmgInfo( this, pAttacker, flCumulativeDamage, nDamageType ); gamestats->Event_WeaponHit( pPlayer, info.m_bPrimaryAttack, pPlayer->GetActiveWeapon()->GetClassname(), dmgInfo ); } + +#ifdef MAPBASE + if ( ai_shot_notify_targets.GetBool() ) + { + if ( IsPlayer() ) + { + // Look for probable target to notify of attack + CBaseEntity *pAimTarget = static_cast(this)->GetProbableAimTarget( info.m_vecSrc, info.m_vecDirShooting ); + if ( pAimTarget && pAimTarget->IsCombatCharacter() ) + { + pAimTarget->MyCombatCharacterPointer()->OnEnemyRangeAttackedMe( this, vecDir, vecEnd ); + } + } + else if ( GetEnemy() && GetEnemy()->IsCombatCharacter() ) + { + GetEnemy()->MyCombatCharacterPointer()->OnEnemyRangeAttackedMe( this, vecDir, vecEnd ); + } + } +#endif #endif } diff --git a/sp/src/game/shared/eventlist.cpp b/sp/src/game/shared/eventlist.cpp index 043293bc..2776aa8f 100644 --- a/sp/src/game/shared/eventlist.cpp +++ b/sp/src/game/shared/eventlist.cpp @@ -233,6 +233,11 @@ void EventList_RegisterSharedEvents( void ) REGISTER_SHARED_ANIMEVENT( AE_SV_DUSTTRAIL, AE_TYPE_SERVER ); REGISTER_SHARED_ANIMEVENT( AE_CL_CREATE_PARTICLE_EFFECT, AE_TYPE_CLIENT ); +#ifdef MAPBASE // From Alien Swarm SDK + REGISTER_SHARED_ANIMEVENT( AE_CL_STOP_PARTICLE_EFFECT, AE_TYPE_CLIENT ); + REGISTER_SHARED_ANIMEVENT( AE_CL_ADD_PARTICLE_EFFECT_CP, AE_TYPE_CLIENT ); + //REGISTER_SHARED_ANIMEVENT( AE_CL_CREATE_PARTICLE_BRASS, AE_TYPE_CLIENT ); +#endif REGISTER_SHARED_ANIMEVENT( AE_RAGDOLL, AE_TYPE_SERVER ); @@ -252,5 +257,8 @@ void EventList_RegisterSharedEvents( void ) #ifdef MAPBASE REGISTER_SHARED_ANIMEVENT( AE_NPC_RESPONSE, AE_TYPE_SERVER ); REGISTER_SHARED_ANIMEVENT( AE_NPC_RESPONSE_FORCED, AE_TYPE_SERVER ); + + REGISTER_SHARED_ANIMEVENT( AE_VSCRIPT_RUN, AE_TYPE_CLIENT | AE_TYPE_SERVER ); + REGISTER_SHARED_ANIMEVENT( AE_VSCRIPT_RUN_FILE, AE_TYPE_CLIENT | AE_TYPE_SERVER ); #endif } \ No newline at end of file diff --git a/sp/src/game/shared/eventlist.h b/sp/src/game/shared/eventlist.h index ee8b38df..7d810bd4 100644 --- a/sp/src/game/shared/eventlist.h +++ b/sp/src/game/shared/eventlist.h @@ -69,6 +69,11 @@ typedef enum AE_SV_DUSTTRAIL, AE_CL_CREATE_PARTICLE_EFFECT, +#ifdef MAPBASE // From Alien Swarm SDK + AE_CL_STOP_PARTICLE_EFFECT, + AE_CL_ADD_PARTICLE_EFFECT_CP, + //AE_CL_CREATE_PARTICLE_BRASS, +#endif AE_RAGDOLL, @@ -88,6 +93,9 @@ typedef enum #ifdef MAPBASE AE_NPC_RESPONSE, // Play a response system concept if we're not speaking AE_NPC_RESPONSE_FORCED, // Always play a response system concept + + AE_VSCRIPT_RUN, // Run vscript code (server + client) + AE_VSCRIPT_RUN_FILE, // Run vscript file (server + client) #endif LAST_SHARED_ANIMEVENT, diff --git a/sp/src/game/shared/mapbase/vscript_singletons.cpp b/sp/src/game/shared/mapbase/vscript_singletons.cpp index eeb50d84..86b1248f 100644 --- a/sp/src/game/shared/mapbase/vscript_singletons.cpp +++ b/sp/src/game/shared/mapbase/vscript_singletons.cpp @@ -22,6 +22,7 @@ #include "filesystem.h" #include "igameevents.h" #include "engine/ivdebugoverlay.h" +#include "icommandline.h" #ifdef CLIENT_DLL #include "IEffects.h" @@ -1094,7 +1095,7 @@ const char *CScriptReadWriteFile::FileRead( const char *szFile ) char pszFullName[MAX_PATH]; V_snprintf( pszFullName, sizeof(pszFullName), SCRIPT_RW_FULL_PATH_FMT, szFile ); - if ( !V_RemoveDotSlashes( pszFullName, CORRECT_PATH_SEPARATOR, true ) ) + if ( !CommandLine()->FindParm( "-script_dotslash_read" ) && !V_RemoveDotSlashes( pszFullName, CORRECT_PATH_SEPARATOR, true ) ) { DevWarning( 2, "Invalid file location : %s\n", szFile ); return NULL; @@ -1143,7 +1144,7 @@ bool CScriptReadWriteFile::FileExists( const char *szFile ) char pszFullName[MAX_PATH]; V_snprintf( pszFullName, sizeof(pszFullName), SCRIPT_RW_FULL_PATH_FMT, szFile ); - if ( !V_RemoveDotSlashes( pszFullName, CORRECT_PATH_SEPARATOR, true ) ) + if ( !CommandLine()->FindParm( "-script_dotslash_read" ) && !V_RemoveDotSlashes( pszFullName, CORRECT_PATH_SEPARATOR, true ) ) { DevWarning( 2, "Invalid file location : %s\n", szFile ); return NULL; @@ -1224,7 +1225,7 @@ HSCRIPT CScriptReadWriteFile::KeyValuesRead( const char *szFile ) char pszFullName[MAX_PATH]; V_snprintf( pszFullName, sizeof(pszFullName), SCRIPT_RW_FULL_PATH_FMT, szFile ); - if ( !V_RemoveDotSlashes( pszFullName, CORRECT_PATH_SEPARATOR, true ) ) + if ( !CommandLine()->FindParm( "-script_dotslash_read" ) && !V_RemoveDotSlashes( pszFullName, CORRECT_PATH_SEPARATOR, true ) ) { DevWarning( 2, "Invalid file location : %s\n", szFile ); return NULL; diff --git a/sp/src/game/shared/mapbase/weapon_custom_scripted.cpp b/sp/src/game/shared/mapbase/weapon_custom_scripted.cpp index f0f7ee83..b988a5a3 100644 --- a/sp/src/game/shared/mapbase/weapon_custom_scripted.cpp +++ b/sp/src/game/shared/mapbase/weapon_custom_scripted.cpp @@ -180,7 +180,7 @@ bool CWeaponCustomScripted::RunWeaponHook( ScriptHook_t &hook, HSCRIPT &cached, { if ( !cached ) { - if ( hook.CanRunInScope( m_ScriptScope ) ) + if ( m_ScriptScope.IsInitialized() && hook.CanRunInScope( m_ScriptScope ) ) { cached = hook.m_hFunc; } @@ -188,6 +188,7 @@ bool CWeaponCustomScripted::RunWeaponHook( ScriptHook_t &hook, HSCRIPT &cached, if (cached) { + hook.m_hFunc = cached; return hook.Call( m_ScriptScope, retVal, pArgs, false ); } @@ -328,7 +329,7 @@ void CWeaponCustomScripted::ItemPreFrame( void ) { SIMPLE_VOID_OVERRIDE( ItemPreFrame, NULL ); - BaseClass::ItemPostFrame(); + BaseClass::ItemPreFrame(); } void CWeaponCustomScripted::ItemPostFrame( void ) diff --git a/sp/src/vscript/vscript_squirrel.nut b/sp/src/vscript/vscript_squirrel.nut index 5b76bbe3..54492423 100644 --- a/sp/src/vscript/vscript_squirrel.nut +++ b/sp/src/vscript/vscript_squirrel.nut @@ -118,15 +118,14 @@ class CSimpleCallChainer function PostScriptExecute() { - local func; - try { - func = scope[prefix]; - } catch(e) { - return; + if ( prefix in scope ) + { + local func = scope[prefix]; + if ( typeof func == "function" ) + { + chain.push(func); + } } - if (typeof(func) != "function") - return; - chain.push(func); } function Call() @@ -569,4 +568,4 @@ if (developer) __Documentation.RegisterHelp( "Lerp", "float Lerp(float, float, float)", "" ); __Documentation.RegisterHelp( "SimpleSpline", "float SimpleSpline(float)", "" ); } -)vscript"; \ No newline at end of file +)vscript";