diff --git a/game/spp_shared/content/hl2epX_extra/scripts/talker/player/humans.txt b/game/spp_shared/content/hl2epX_extra/scripts/talker/player/humans.txt index 05309315a..670dda644 100644 --- a/game/spp_shared/content/hl2epX_extra/scripts/talker/player/humans.txt +++ b/game/spp_shared/content/hl2epX_extra/scripts/talker/player/humans.txt @@ -480,13 +480,13 @@ rule PlayerCitizenStartCombatTurretCeiling response PlayerResponseFollow { - scene "scenes/npc/$gender01/squad_away03.vcd" weight 4 - scene "scenes/npc/$gender01/squad_follow02.vcd" - scene "scenes/npc/$gender01/squad_follow03.vcd" - scene "scenes/npc/$gender01/squad_away01.vcd" weight 3 - scene "scenes/npc/$gender01/squad_away02.vcd" + scene "scenes/npc/$gender01/squad_away03.vcd" weight 4 then any TLK_PLAYER_LEAD foo:0 0.5 + scene "scenes/npc/$gender01/squad_follow02.vcd" then any TLK_PLAYER_LEAD foo:0 0.5 + scene "scenes/npc/$gender01/squad_follow03.vcd" then any TLK_PLAYER_LEAD foo:0 0.5 + scene "scenes/npc/$gender01/squad_away01.vcd" weight 3 then any TLK_PLAYER_LEAD foo:0 0.5 + scene "scenes/npc/$gender01/squad_away02.vcd" then any TLK_PLAYER_LEAD foo:0 0.5 //Trav|Edt - add more Follow scenes - scene "scenes/npc/$gender01/overhere01.vcd" + scene "scenes/npc/$gender01/overhere01.vcd" then any TLK_PLAYER_LEAD foo:0 0.5 } rule PlayerFollow diff --git a/src/game/client/client_base.vpc b/src/game/client/client_base.vpc index 387247831..a5456862e 100644 --- a/src/game/client/client_base.vpc +++ b/src/game/client/client_base.vpc @@ -672,7 +672,6 @@ $Project "$SRCDIR\public\dt_utlvector_recv.cpp" \ "$SRCDIR\public\filesystem_helpers.cpp" \ "$SRCDIR\public\interpolatortypes.cpp" \ - "$SRCDIR\game\shared\interval.cpp" \ "$SRCDIR\common\language.cpp" \ "$SRCDIR\public\networkvar.cpp" \ "$SRCDIR\common\randoverride.cpp" \ @@ -1245,6 +1244,7 @@ $Project $File "$SRCDIR\public\vgui_controls\WizardSubPanel.h" $File "$SRCDIR\public\worldsize.h" $File "$SRCDIR\public\zip_uncompressed.h" + $File "$SRCDIR\public\tier1\interval.h" //Haptics $File "$SRCDIR\public\haptics\ihaptics.h" [$WIN32] $File "$SRCDIR\public\haptics\haptic_utils.h" [$WIN32] @@ -1301,7 +1301,6 @@ $Project $File "$SRCDIR\game\shared\igamesystem.h" $File "$SRCDIR\game\shared\imovehelper.h" $File "$SRCDIR\game\shared\in_buttons.h" - $File "$SRCDIR\game\shared\interval.h" $File "$SRCDIR\game\shared\iplayeranimstate.h" $File "$SRCDIR\game\shared\ipredictionsystem.h" $File "$SRCDIR\game\shared\itempents.h" diff --git a/src/game/server/AI_Criteria.cpp b/src/game/server/AI_Criteria.cpp index 128e9d324..31a5b1eff 100644 --- a/src/game/server/AI_Criteria.cpp +++ b/src/game/server/AI_Criteria.cpp @@ -1,174 +1,24 @@ -//========= Copyright Valve Corporation, All rights reserved. ============// +//===== Copyright © 1996-2005, Valve Corporation, All rights reserved. ======// // // Purpose: // // $NoKeywords: $ // -//=============================================================================// +//===========================================================================// #include "cbase.h" -#include "AI_Criteria.h" +#include "ai_criteria.h" + +#ifdef GAME_DLL #include "ai_speech.h" -#include -#include "engine/IEngineSound.h" +#endif + +#include +#include "engine/ienginesound.h" // memdbgon must be the last include file in a .cpp file!!! #include -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -AI_CriteriaSet::AI_CriteriaSet() : m_Lookup( 0, 0, CritEntry_t::LessFunc ) -{ -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : src - -//----------------------------------------------------------------------------- -AI_CriteriaSet::AI_CriteriaSet( const AI_CriteriaSet& src ) : m_Lookup( 0, 0, CritEntry_t::LessFunc ) -{ - // Use fast Copy CUtlRBTree CopyFrom. WARNING: It only handles POD. - m_Lookup.CopyFrom( src.m_Lookup ); -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -AI_CriteriaSet::~AI_CriteriaSet() -{ -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *criteria - -// "" - -// 1.0f - -//----------------------------------------------------------------------------- -void AI_CriteriaSet::AppendCriteria( const char *criteria, const char *value /*= ""*/, float weight /*= 1.0f*/ ) -{ - // Note: value pointer may come from an entry inside m_Lookup! - // that value string must be copied out before any modification - // to the m_Lookup struct which could make the pointer invalid - int idx = FindCriterionIndex( criteria ); - if ( idx == -1 ) - { - CritEntry_t entry; - entry.criterianame = criteria; - MEM_ALLOC_CREDIT(); - entry.SetValue(value); - entry.weight = weight; - m_Lookup.Insert( entry ); - } - else - { - CritEntry_t *entry = &m_Lookup[ idx ]; - entry->SetValue( value ); - entry->weight = weight; - } -} - - -//----------------------------------------------------------------------------- -// Removes criteria in a set -//----------------------------------------------------------------------------- -void AI_CriteriaSet::RemoveCriteria( const char *criteria ) -{ - int idx = FindCriterionIndex( criteria ); - if ( idx == -1 ) - return; - - m_Lookup.RemoveAt( idx ); -} - - -//----------------------------------------------------------------------------- -// Purpose: -// Output : int -//----------------------------------------------------------------------------- -int AI_CriteriaSet::GetCount() const -{ - return m_Lookup.Count(); -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *name - -// Output : int -//----------------------------------------------------------------------------- -int AI_CriteriaSet::FindCriterionIndex( const char *name ) const -{ - CritEntry_t search; - search.criterianame = name; - int idx = m_Lookup.Find( search ); - if ( idx == m_Lookup.InvalidIndex() ) - return -1; - - return idx; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : index - -// Output : char const -//----------------------------------------------------------------------------- -const char *AI_CriteriaSet::GetName( int index ) const -{ - static char namebuf[ 128 ]; - if ( index < 0 || index >= (int)m_Lookup.Count() ) - return ""; - - const CritEntry_t *entry = &m_Lookup[ index ]; - Q_strncpy( namebuf, entry->criterianame.String(), sizeof( namebuf ) ); - return namebuf; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : index - -// Output : char const -//----------------------------------------------------------------------------- -const char *AI_CriteriaSet::GetValue( int index ) const -{ - if ( index < 0 || index >= (int)m_Lookup.Count() ) - return ""; - - const CritEntry_t *entry = &m_Lookup[ index ]; - return entry->value ? entry->value : ""; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : index - -// Output : float -//----------------------------------------------------------------------------- -float AI_CriteriaSet::GetWeight( int index ) const -{ - if ( index < 0 || index >= (int)m_Lookup.Count() ) - return 1.0f; - - const CritEntry_t *entry = &m_Lookup[ index ]; - return entry->weight; -} -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void AI_CriteriaSet::Describe() -{ - for ( short i = m_Lookup.FirstInorder(); i != m_Lookup.InvalidIndex(); i = m_Lookup.NextInorder( i ) ) - { - CritEntry_t *entry = &m_Lookup[ i ]; - - if ( entry->weight != 1.0f ) - { - DevMsg( " %20s = '%s' (weight %f)\n", entry->criterianame.String(), entry->value ? entry->value : "", entry->weight ); - } - else - { - DevMsg( " %20s = '%s'\n", entry->criterianame.String(), entry->value ? entry->value : "" ); - } - } -} BEGIN_SIMPLE_DATADESC( AI_ResponseParams ) DEFINE_FIELD( flags, FIELD_SHORT ), @@ -186,316 +36,3 @@ BEGIN_SIMPLE_DATADESC( AI_Response ) DEFINE_EMBEDDED( m_Params ), END_DATADESC() -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -AI_Response::AI_Response() -{ - m_Type = RESPONSE_NONE; - m_szResponseName[0] = 0; - m_szMatchingRule[0] = 0; - - m_pCriteria = NULL; - m_bApplyContextToWorld = false; -} - -//----------------------------------------------------------------------------- -//----------------------------------------------------------------------------- -AI_Response::AI_Response( const AI_Response &from ) -{ - m_pCriteria = NULL; - *this = from; -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -AI_Response::~AI_Response() -{ - delete m_pCriteria; - m_pCriteria = NULL; -} - -//----------------------------------------------------------------------------- -AI_Response &AI_Response::operator=( const AI_Response &from ) -{ - Assert( (void*)(&m_Type) == (void*)this ); - - if (this == &from) - return *this; - - m_Type = from.m_Type; - - V_strcpy_safe( m_szResponseName, from.m_szResponseName ); - V_strcpy_safe( m_szMatchingRule, from.m_szMatchingRule ); - - delete m_pCriteria; - m_pCriteria = NULL; - - // Copy criteria. - if (from.m_pCriteria) - m_pCriteria = new AI_CriteriaSet(*from.m_pCriteria); - - m_Params = from.m_Params; - - m_szContext = from.m_szContext; - m_bApplyContextToWorld = from.m_bApplyContextToWorld; - - return *this; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *response - -// *criteria - -//----------------------------------------------------------------------------- -void AI_Response::Init( ResponseType_t type, const char *responseName, const AI_CriteriaSet& criteria, - const AI_ResponseParams& responseparams, const char *ruleName, const char *applyContext, - bool bApplyContextToWorld ) -{ - m_Type = type; - - V_strcpy_safe( m_szResponseName, responseName ); - V_strcpy_safe( m_szMatchingRule, ruleName ? ruleName : "NULL" ); - - // Copy underlying criteria - Assert( !m_pCriteria ); - m_pCriteria = new AI_CriteriaSet( criteria ); - - m_Params = responseparams; - - m_szContext = applyContext; - m_bApplyContextToWorld = bApplyContextToWorld; -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void AI_Response::Describe() -{ - if ( m_pCriteria ) - { - DevMsg( "Search criteria:\n" ); - m_pCriteria->Describe(); - } - if ( m_szMatchingRule[ 0 ] ) - DevMsg( "Matched rule '%s', ", m_szMatchingRule ); - if ( m_szContext.Length() ) - DevMsg( "Contexts to set '%s' on %s, ", m_szContext.Get(), m_bApplyContextToWorld ? "world" : "speaker" ); - - DevMsg( "response %s = '%s'\n", DescribeResponse( (ResponseType_t)m_Type ), m_szResponseName ); -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -const char * AI_Response::GetNamePtr() const -{ - return m_szResponseName; -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -const char * AI_Response::GetResponsePtr() const -{ - return m_szResponseName; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : type - -// Output : char const -//----------------------------------------------------------------------------- -const char *AI_Response::DescribeResponse( ResponseType_t type ) -{ - if ( (int)type < 0 || (int)type >= NUM_RESPONSES ) - { - Assert( 0 ); - return "???AI_Response bogus index"; - } - - switch( type ) - { - case RESPONSE_NONE: return "RESPONSE_NONE"; - case RESPONSE_SPEAK: return "RESPONSE_SPEAK"; - case RESPONSE_SENTENCE: return "RESPONSE_SENTENCE"; - case RESPONSE_SCENE: return "RESPONSE_SCENE"; - case RESPONSE_RESPONSE: return "RESPONSE_RESPONSE"; - case RESPONSE_PRINT: return "RESPONSE_PRINT"; - } - - Assert( 0 ); - return "RESPONSE_NONE"; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Output : const AI_CriteriaSet -//----------------------------------------------------------------------------- -const AI_CriteriaSet *AI_Response::GetCriteria() -{ - return m_pCriteria; -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void AI_Response::Release() -{ - delete this; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Output : soundlevel_t -//----------------------------------------------------------------------------- -soundlevel_t AI_Response::GetSoundLevel() const -{ - if ( m_Params.flags & AI_ResponseParams::RG_SOUNDLEVEL ) - { - return (soundlevel_t)m_Params.soundlevel; - } - - return SNDLVL_TALKING; -} - -float AI_Response::GetRespeakDelay( void ) const -{ - if ( m_Params.flags & AI_ResponseParams::RG_RESPEAKDELAY ) - { - interval_t temp; - m_Params.respeakdelay.ToInterval( temp ); - return RandomInterval( temp ); - } - - return 0.0f; -} - -float AI_Response::GetWeaponDelay( void ) const -{ - if ( m_Params.flags & AI_ResponseParams::RG_WEAPONDELAY ) - { - interval_t temp; - m_Params.weapondelay.ToInterval( temp ); - return RandomInterval( temp ); - } - - return 0.0f; -} - -bool AI_Response::GetSpeakOnce( void ) const -{ - if ( m_Params.flags & AI_ResponseParams::RG_SPEAKONCE ) - { - return true; - } - - return false; -} - -bool AI_Response::ShouldntUseScene( void ) const -{ - return ( m_Params.flags & AI_ResponseParams::RG_DONT_USE_SCENE ) != 0; -} - -bool AI_Response::ShouldBreakOnNonIdle( void ) const -{ - return ( m_Params.flags & AI_ResponseParams::RG_STOP_ON_NONIDLE ) != 0; -} - -int AI_Response::GetOdds( void ) const -{ - if ( m_Params.flags & AI_ResponseParams::RG_ODDS ) - { - return m_Params.odds; - } - return 100; -} - -float AI_Response::GetDelay() const -{ - if ( m_Params.flags & AI_ResponseParams::RG_DELAYAFTERSPEAK ) - { - interval_t temp; - m_Params.delay.ToInterval( temp ); - return RandomInterval( temp ); - } - return 0.0f; -} - -float AI_Response::GetPreDelay() const -{ - if ( m_Params.flags & AI_ResponseParams::RG_DELAYBEFORESPEAK ) - { - interval_t temp; - m_Params.predelay.ToInterval( temp ); - return RandomInterval( temp ); - } - return 0.0f; -} - -//----------------------------------------------------------------------------- -// Purpose: Sets context string -// Output : void -//----------------------------------------------------------------------------- -void AI_Response::SetContext( const char *context ) -{ - m_szContext = context; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *raw - -// *key - -// keylen - -// *value - -// valuelen - -// *duration - -// Output : static bool -//----------------------------------------------------------------------------- -const char *SplitContext( const char *raw, char *key, int keylen, char *value, int valuelen, float *duration ) -{ - char *colon1 = Q_strstr( raw, ":" ); - if ( !colon1 ) - { - DevMsg( "SplitContext: warning, ignoring context '%s', missing colon separator!\n", raw ); - *key = *value = 0; - return NULL; - } - - int len = colon1 - raw; - Q_strncpy( key, raw, MIN( len + 1, keylen ) ); - key[ MIN( len, keylen - 1 ) ] = 0; - - bool last = false; - char *end = Q_strstr( colon1 + 1, "," ); - if ( !end ) - { - int remaining = Q_strlen( colon1 + 1 ); - end = colon1 + 1 + remaining; - last = true; - } - - char *colon2 = Q_strstr( colon1 + 1, ":" ); - if ( colon2 && ( colon2 < end ) ) - { - if ( duration ) - *duration = atof( colon2 + 1 ); - - len = MIN( colon2 - ( colon1 + 1 ), valuelen - 1 ); - Q_strncpy( value, colon1 + 1, len + 1 ); - value[ len ] = 0; - } - else - { - if ( duration ) - *duration = 0.0; - - len = MIN( end - ( colon1 + 1 ), valuelen - 1 ); - Q_strncpy( value, colon1 + 1, len + 1 ); - value[ len ] = 0; - } - - return last ? NULL : end + 1; -} diff --git a/src/game/server/AI_Criteria.h b/src/game/server/AI_Criteria.h index f10c2d05b..b5d2c4fd6 100644 --- a/src/game/server/AI_Criteria.h +++ b/src/game/server/AI_Criteria.h @@ -1,4 +1,4 @@ -//========= Copyright Valve Corporation, All rights reserved. ============// +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // @@ -12,228 +12,30 @@ #include "tier1/utlrbtree.h" #include "tier1/utlsymbol.h" -#include "interval.h" +#include "tier1/interval.h" #include "mathlib/compressed_vector.h" +#include "../../public/responserules/response_types.h" -extern const char *SplitContext( const char *raw, char *key, int keylen, char *value, int valuelen, float *duration ); +using ResponseRules::ResponseType_t; -class AI_CriteriaSet -{ -public: - AI_CriteriaSet(); - AI_CriteriaSet( const AI_CriteriaSet& src ); - ~AI_CriteriaSet(); - - void AppendCriteria( const char *criteria, const char *value = "", float weight = 1.0f ); - void RemoveCriteria( const char *criteria ); - - void Describe(); - - int GetCount() const; - int FindCriterionIndex( const char *name ) const; - - const char *GetName( int index ) const; - const char *GetValue( int index ) const; - float GetWeight( int index ) const; - -private: - - struct CritEntry_t - { - CritEntry_t() : - criterianame( UTL_INVAL_SYMBOL ), - weight( 0.0f ) - { - value[ 0 ] = 0; - } - - CritEntry_t( const CritEntry_t& src ) - { - criterianame = src.criterianame; - value[ 0 ] = 0; - weight = src.weight; - SetValue( src.value ); - } - - CritEntry_t& operator=( const CritEntry_t& src ) - { - if ( this == &src ) - return *this; - - criterianame = src.criterianame; - weight = src.weight; - SetValue( src.value ); - - return *this; - } - - static bool LessFunc( const CritEntry_t& lhs, const CritEntry_t& rhs ) - { - return Q_stricmp( lhs.criterianame.String(), rhs.criterianame.String() ) < 0 ? true : false; - } - - void SetValue( char const *str ) - { - if ( !str ) - { - value[ 0 ] = 0; - } - else - { - Q_strncpy( value, str, sizeof( value ) ); - } - } - - // We use CUtlRBTree CopyFrom() in ctor, so CritEntry_t must be POD. If you add - // CUtlString or something then you must change AI_CriteriaSet copy ctor. - CUtlSymbol criterianame; - char value[ 64 ]; - float weight; - }; - - CUtlRBTree< CritEntry_t, short > m_Lookup; -}; - -#pragma pack(1) -template -struct response_interval_t -{ - T start; - T range; - - interval_t &ToInterval( interval_t &dest ) const { dest.start = start; dest.range = range; return dest; } - void FromInterval( const interval_t &from ) { start = from.start; range = from.range; } - float Random() const { interval_t temp = { start, range }; return RandomInterval( temp ); } -}; - -typedef response_interval_t responseparams_interval_t; - -struct AI_ResponseParams -{ - DECLARE_SIMPLE_DATADESC(); +extern const char *SplitContext( const char *raw, char *key, int keylen, char *value, int valuelen, float *duration, const char *entireContext ); - enum - { - RG_DELAYAFTERSPEAK = (1<<0), - RG_SPEAKONCE = (1<<1), - RG_ODDS = (1<<2), - RG_RESPEAKDELAY = (1<<3), - RG_SOUNDLEVEL = (1<<4), - RG_DONT_USE_SCENE = (1<<5), - RG_STOP_ON_NONIDLE = (1<<6), - RG_WEAPONDELAY = (1<<7), - RG_DELAYBEFORESPEAK = (1<<8), - }; - - AI_ResponseParams() - { - flags = 0; - odds = 100; - delay.start = 0; - delay.range = 0; - respeakdelay.start = 0; - respeakdelay.range = 0; - weapondelay.start = 0; - weapondelay.range = 0; - soundlevel = 0; - predelay.start = 0; - predelay.range = 0; - } - - responseparams_interval_t delay; //4 - responseparams_interval_t respeakdelay; //8 - responseparams_interval_t weapondelay; //12 +#ifndef AI_CriteriaSet +#define AI_CriteriaSet ResponseRules::CriteriaSet +#endif - short odds; //14 +typedef ResponseRules::ResponseParams AI_ResponseParams ; +typedef ResponseRules::CRR_Response AI_Response; - short flags; //16 - byte soundlevel; //17 - responseparams_interval_t predelay; //21 -}; -#pragma pack() -//----------------------------------------------------------------------------- -// Purpose: Generic container for a response to a match to a criteria set -// This is what searching for a response returns -//----------------------------------------------------------------------------- -enum ResponseType_t +/* +// An AI response that is dynamically new'ed up and returned from SpeakFindResponse. +class AI_ResponseReturnValue : AI_Response { - RESPONSE_NONE = 0, - RESPONSE_SPEAK, - RESPONSE_SENTENCE, - RESPONSE_SCENE, - RESPONSE_RESPONSE, // A reference to another response by name - RESPONSE_PRINT, - - NUM_RESPONSES, -}; - -class AI_Response -{ -public: - DECLARE_SIMPLE_DATADESC(); - - AI_Response(); - AI_Response( const AI_Response &from ); - ~AI_Response(); - AI_Response &operator=( const AI_Response &from ); - - void Release(); - - const char * GetNamePtr() const; - const char * GetResponsePtr() const; - const AI_ResponseParams *GetParams() const { return &m_Params; } - ResponseType_t GetType() const { return (ResponseType_t)m_Type; } - soundlevel_t GetSoundLevel() const; - float GetRespeakDelay() const; - float GetWeaponDelay() const; - bool GetSpeakOnce() const; - bool ShouldntUseScene( ) const; - bool ShouldBreakOnNonIdle( void ) const; - int GetOdds() const; - float GetDelay() const; - float GetPreDelay() const; - - void SetContext( const char *context ); - const char * GetContext( void ) const { return m_szContext.Length() ? m_szContext.Get() : NULL; } - - bool IsApplyContextToWorld( void ) { return m_bApplyContextToWorld; } - - void Describe(); - - const AI_CriteriaSet* GetCriteria(); - - void Init( ResponseType_t type, - const char *responseName, - const AI_CriteriaSet& criteria, - const AI_ResponseParams& responseparams, - const char *matchingRule, - const char *applyContext, - bool bApplyContextToWorld ); - - static const char *DescribeResponse( ResponseType_t type ); - - enum - { - MAX_RESPONSE_NAME = 64, - MAX_RULE_NAME = 64 - }; - - -private: - byte m_Type; - char m_szResponseName[ MAX_RESPONSE_NAME ]; - char m_szMatchingRule[ MAX_RULE_NAME ]; - - // The initial criteria to which we are responsive - AI_CriteriaSet *m_pCriteria; - - AI_ResponseParams m_Params; - CUtlString m_szContext; - bool m_bApplyContextToWorld; }; +*/ #endif // AI_CRITERIA_H diff --git a/src/game/server/AI_ResponseSystem.cpp b/src/game/server/AI_ResponseSystem.cpp index 8a6b0db5b..0d791c39b 100644 --- a/src/game/server/AI_ResponseSystem.cpp +++ b/src/game/server/AI_ResponseSystem.cpp @@ -1,4 +1,4 @@ -//========= Copyright Valve Corporation, All rights reserved. ============// +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // @@ -6,732 +6,192 @@ #include "cbase.h" -#include "SoundEmitterSystem/isoundemittersystembase.h" -#include "AI_ResponseSystem.h" +#include "soundemittersystem/isoundemittersystembase.h" +#include "ai_responsesystem.h" #include "igamesystem.h" -#include "AI_Criteria.h" -#include +#include "ai_criteria.h" +#include #include "filesystem.h" #include "utldict.h" +#ifdef GAME_DLL #include "ai_speech.h" +#endif #include "tier0/icommandline.h" #include -#include "sceneentity.h" #include "isaverestore.h" #include "utlbuffer.h" #include "stringpool.h" #include "fmtstr.h" #include "multiplay_gamerules.h" +#include "characterset.h" +#include "responserules/response_host_interface.h" +#include "../../responserules/runtime/response_types_internal.h" + +#include "scenefilecache/ISceneFileCache.h" +#include "tier1/generichash.h" + +#ifdef GAME_DLL +#include "sceneentity.h" +#endif + +#include "networkstringtabledefs.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" -ConVar rr_debugresponses( "rr_debugresponses", "0", FCVAR_NONE, "Show verbose matching output (1 for simple, 2 for rule scoring). If set to 3, it will only show response success/failure for npc_selected NPCs." ); -ConVar rr_debugrule( "rr_debugrule", "", FCVAR_NONE, "If set to the name of the rule, that rule's score will be shown whenever a concept is passed into the response rules system."); -ConVar rr_dumpresponses( "rr_dumpresponses", "0", FCVAR_NONE, "Dump all response_rules.txt and rules (requires restart)" ); +using namespace ResponseRules; -static CUtlSymbolTable g_RS; +extern ConVar rr_debugresponses; // ( "rr_debugresponses", "0", FCVAR_NONE, "Show verbose matching output (1 for simple, 2 for rule scoring, 3 for noisy). If set to 4, it will only show response success/failure for npc_selected NPCs." ); +extern ConVar rr_debugrule; // ( "rr_debugrule", "", FCVAR_NONE, "If set to the name of the rule, that rule's score will be shown whenever a concept is passed into the response rules system."); +extern ConVar rr_dumpresponses; // ( "rr_dumpresponses", "0", FCVAR_NONE, "Dump all response_rules.txt and rules (requires restart)" ); +extern ConVar rr_debugresponseconcept; // ( "rr_debugresponseconcept", "", FCVAR_NONE, "If set, rr_debugresponses will print only responses testing for the specified concept" ); -inline static char *CopyString( const char *in ) -{ - if ( !in ) - return NULL; +extern ISceneFileCache *scenefilecache; +extern INetworkStringTable *g_pStringTableClientSideChoreoScenes; - int len = Q_strlen( in ); - char *out = new char[ len + 1 ]; - Q_memcpy( out, in, len ); - out[ len ] = 0; - return out; -} +static characterset_t g_BreakSetIncludingColons; -#pragma pack(1) -class Matcher +// Simple class to initialize breakset +class CBreakInit { public: - Matcher() - { - valid = false; - isnumeric = false; - notequal = false; - usemin = false; - minequals = false; - usemax = false; - maxequals = false; - maxval = 0.0f; - minval = 0.0f; - - token = UTL_INVAL_SYMBOL; - rawtoken = UTL_INVAL_SYMBOL; - } - - void Describe( void ) - { - if ( !valid ) - { - DevMsg( " invalid!\n" ); - return; - } - char sz[ 128 ]; - - sz[ 0] = 0; - int minmaxcount = 0; - if ( usemin ) - { - Q_snprintf( sz, sizeof( sz ), ">%s%.3f", minequals ? "=" : "", minval ); - minmaxcount++; - } - if ( usemax ) - { - char sz2[ 128 ]; - Q_snprintf( sz2, sizeof( sz2 ), "<%s%.3f", maxequals ? "=" : "", maxval ); - - if ( minmaxcount > 0 ) - { - Q_strncat( sz, " and ", sizeof( sz ), COPY_ALL_CHARACTERS ); - } - Q_strncat( sz, sz2, sizeof( sz ), COPY_ALL_CHARACTERS ); - minmaxcount++; - } - - if ( minmaxcount >= 1 ) - { - DevMsg( " matcher: %s\n", sz ); - return; - } - - if ( notequal ) - { - DevMsg( " matcher: !=%s\n", GetToken() ); - return; - } - - DevMsg( " matcher: ==%s\n", GetToken() ); - } - - float maxval; - float minval; - - bool valid : 1; //1 - bool isnumeric : 1; //2 - bool notequal : 1; //3 - bool usemin : 1; //4 - bool minequals : 1; //5 - bool usemax : 1; //6 - bool maxequals : 1; //7 - - void SetToken( char const *s ) - { - token = g_RS.AddString( s ); - } - - char const *GetToken() - { - if ( token.IsValid() ) - { - return g_RS.String( token ); - } - return ""; - } - void SetRaw( char const *raw ) - { - rawtoken = g_RS.AddString( raw ); - } - char const *GetRaw() + CBreakInit() { - if ( rawtoken.IsValid() ) - { - return g_RS.String( rawtoken ); - } - return ""; + CharacterSetBuild( &g_BreakSetIncludingColons, "{}()':" ); } +} g_BreakInit; -private: - CUtlSymbol token; - CUtlSymbol rawtoken; -}; - -struct Response +inline char rr_tolower( char c ) { - DECLARE_SIMPLE_DATADESC(); - - Response() - { - type = RESPONSE_NONE; - value = NULL; - weight.SetFloat( 1.0f ); - depletioncount = 0; - first = false; - last = false; - } - - Response( const Response& src ) - { - weight = src.weight; - type = src.type; - value = CopyString( src.value ); - depletioncount = src.depletioncount; - first = src.first; - last = src.last; - } - - Response& operator =( const Response& src ) - { - if ( this == &src ) - return *this; - weight = src.weight; - type = src.type; - value = CopyString( src.value ); - depletioncount = src.depletioncount; - first = src.first; - last = src.last; - return *this; - } - - ~Response() - { - delete[] value; - } - - ResponseType_t GetType() { return (ResponseType_t)type; } - - char *value; // fixed up value spot // 4 - float16 weight; // 6 - - byte depletioncount; // 7 - byte type : 6; // 8 - byte first : 1; // - byte last : 1; // -}; - -struct ResponseGroup + if ( c >= 'A' && c <= 'Z' ) + return c - 'A' + 'a'; + return c; +} +// BUG BUG: Note that this function doesn't check for data overruns!!! +// Also, this function lowercases the token as it parses!!! +inline const char *RR_Parse(const char *data, char *token ) { - DECLARE_SIMPLE_DATADESC(); - - ResponseGroup() - { - // By default visit all nodes before repeating - m_bSequential = false; - m_bNoRepeat = false; - m_bEnabled = true; - m_nCurrentIndex = 0; - m_bDepleteBeforeRepeat = true; - m_nDepletionCount = 1; - m_bHasFirst = false; - m_bHasLast = false; - } - - ResponseGroup( const ResponseGroup& src ) - { - int c = src.group.Count(); - for ( int i = 0; i < c; i++ ) - { - group.AddToTail( src.group[ i ] ); - } - - rp = src.rp; - m_bDepleteBeforeRepeat = src.m_bDepleteBeforeRepeat; - m_nDepletionCount = src.m_nDepletionCount; - m_bHasFirst = src.m_bHasFirst; - m_bHasLast = src.m_bHasLast; - m_bSequential = src.m_bSequential; - m_bNoRepeat = src.m_bNoRepeat; - m_bEnabled = src.m_bEnabled; - m_nCurrentIndex = src.m_nCurrentIndex; - } - - ResponseGroup& operator=( const ResponseGroup& src ) - { - if ( this == &src ) - return *this; - int c = src.group.Count(); - for ( int i = 0; i < c; i++ ) - { - group.AddToTail( src.group[ i ] ); - } - - rp = src.rp; - m_bDepleteBeforeRepeat = src.m_bDepleteBeforeRepeat; - m_nDepletionCount = src.m_nDepletionCount; - m_bHasFirst = src.m_bHasFirst; - m_bHasLast = src.m_bHasLast; - m_bSequential = src.m_bSequential; - m_bNoRepeat = src.m_bNoRepeat; - m_bEnabled = src.m_bEnabled; - m_nCurrentIndex = src.m_nCurrentIndex; - return *this; - } - - bool HasUndepletedChoices() const - { - if ( !m_bDepleteBeforeRepeat ) - return true; - - int c = group.Count(); - for ( int i = 0; i < c; i++ ) - { - if ( group[ i ].depletioncount != m_nDepletionCount ) - return true; - } - - return false; - } - - void MarkResponseUsed( int idx ) - { - if ( !m_bDepleteBeforeRepeat ) - return; - - if ( idx < 0 || idx >= group.Count() ) - { - Assert( 0 ); - return; - } + unsigned char c; + int len; + characterset_t *breaks = &g_BreakSetIncludingColons; + len = 0; + token[0] = 0; - group[ idx ].depletioncount = m_nDepletionCount; - } + if (!data) + return NULL; - void ResetDepletionCount() + // skip whitespace +skipwhite: + while ( (c = *data) <= ' ') { - if ( !m_bDepleteBeforeRepeat ) - return; - ++m_nDepletionCount; + if (c == 0) + return NULL; // end of file; + data++; } - void Reset() + // skip // comments + if (c=='/' && data[1] == '/') { - ResetDepletionCount(); - SetEnabled( true ); - SetCurrentIndex( 0 ); - m_nDepletionCount = 1; - - for ( int i = 0; i < group.Count(); ++i ) - { - group[ i ].depletioncount = 0; - } + while (*data && *data != '\n') + data++; + goto skipwhite; } - bool HasUndepletedFirst( int& index ) - { - index = -1; - - if ( !m_bDepleteBeforeRepeat ) - return false; - - int c = group.Count(); - for ( int i = 0; i < c; i++ ) - { - Response *r = &group[ i ]; - - if ( ( r->depletioncount != m_nDepletionCount ) && r->first ) - { - index = i; - return true; - } - } - return false; - } - - bool HasUndepletedLast( int& index ) + // handle quoted strings specially + if (c == '\"') { - index = -1; - - if ( !m_bDepleteBeforeRepeat ) - return false; - - int c = group.Count(); - for ( int i = 0; i < c; i++ ) + data++; + while (1) { - Response *r = &group[ i ]; - - if ( ( r->depletioncount != m_nDepletionCount ) && r->last ) + c = rr_tolower( *data++ ); + if (c=='\"' || !c) { - index = i; - return true; + token[len] = 0; + return data; } + token[len] = c; + len++; } - - return false; - } - - bool ShouldCheckRepeats() const { return m_bDepleteBeforeRepeat; } - int GetDepletionCount() const { return m_nDepletionCount; } - - bool IsSequential() const { return m_bSequential; } - void SetSequential( bool seq ) { m_bSequential = seq; } - - bool IsNoRepeat() const { return m_bNoRepeat; } - void SetNoRepeat( bool norepeat ) { m_bNoRepeat = norepeat; } - - bool IsEnabled() const { return m_bEnabled; } - void SetEnabled( bool enabled ) { m_bEnabled = enabled; } - - int GetCurrentIndex() const { return m_nCurrentIndex; } - void SetCurrentIndex( byte idx ) { m_nCurrentIndex = idx; } - - CUtlVector< Response > group; - - AI_ResponseParams rp; - - bool m_bEnabled; - - byte m_nCurrentIndex; - // Invalidation counter - byte m_nDepletionCount; - - // Use all slots before repeating any - bool m_bDepleteBeforeRepeat : 1; - bool m_bHasFirst : 1; - bool m_bHasLast : 1; - bool m_bSequential : 1; - bool m_bNoRepeat : 1; - -}; - -struct Criteria -{ - Criteria() - { - name = NULL; - value = NULL; - weight.SetFloat( 1.0f ); - required = false; - } - Criteria& operator =(const Criteria& src ) - { - if ( this == &src ) - return *this; - - name = CopyString( src.name ); - value = CopyString( src.value ); - weight = src.weight; - required = src.required; - - matcher = src.matcher; - - int c = src.subcriteria.Count(); - for ( int i = 0; i < c; i++ ) - { - subcriteria.AddToTail( src.subcriteria[ i ] ); - } - - return *this; } - Criteria(const Criteria& src ) - { - name = CopyString( src.name ); - value = CopyString( src.value ); - weight = src.weight; - required = src.required; - - matcher = src.matcher; - int c = src.subcriteria.Count(); - for ( int i = 0; i < c; i++ ) - { - subcriteria.AddToTail( src.subcriteria[ i ] ); - } - } - ~Criteria() + // parse single characters + if ( IN_CHARACTERSET( *breaks, c ) ) { - delete[] name; - delete[] value; + token[len] = c; + len++; + token[len] = 0; + return data+1; } - bool IsSubCriteriaType() const + // parse a regular word + do { - return ( subcriteria.Count() > 0 ) ? true : false; - } - - char *name; - char *value; - float16 weight; - bool required; - - Matcher matcher; + token[len] = rr_tolower( c ); + data++; + len++; + c = rr_tolower( *data ); + if ( IN_CHARACTERSET( *breaks, c ) ) + break; + } while (c>32); - // Indices into sub criteria - CUtlVector< unsigned short > subcriteria; -}; + token[len] = 0; + return data; +} -struct Rule +namespace ResponseRules { - Rule() - { - m_bMatchOnce = false; - m_bEnabled = true; - m_szContext = NULL; - m_bApplyContextToWorld = false; - } - - Rule& operator =( const Rule& src ) - { - if ( this == &src ) - return *this; - - int i; - int c; - - c = src.m_Criteria.Count(); - for ( i = 0; i < c; i++ ) - { - m_Criteria.AddToTail( src.m_Criteria[ i ] ); - } - - c = src.m_Responses.Count(); - for ( i = 0; i < c; i++ ) - { - m_Responses.AddToTail( src.m_Responses[ i ] ); - } - - SetContext( src.m_szContext ); - m_bMatchOnce = src.m_bMatchOnce; - m_bEnabled = src.m_bEnabled; - m_bApplyContextToWorld = src.m_bApplyContextToWorld; - return *this; - } - - Rule( const Rule& src ) - { - int i; - int c; - - c = src.m_Criteria.Count(); - for ( i = 0; i < c; i++ ) - { - m_Criteria.AddToTail( src.m_Criteria[ i ] ); - } - - c = src.m_Responses.Count(); - for ( i = 0; i < c; i++ ) - { - m_Responses.AddToTail( src.m_Responses[ i ] ); - } - - SetContext( src.m_szContext ); - m_bMatchOnce = src.m_bMatchOnce; - m_bEnabled = src.m_bEnabled; - m_bApplyContextToWorld = src.m_bApplyContextToWorld; - } + extern const char *ResponseCopyString( const char *in ); +} - ~Rule() +// Host functions required by the ResponseRules::IEngineEmulator interface +class CResponseRulesToEngineInterface : public ResponseRules::IEngineEmulator +{ + /// Given an input text buffer data pointer, parses a single token into the variable token and returns the new + /// reading position + virtual const char *ParseFile( const char *data, char *token, int maxlen ) { - delete[] m_szContext; + NOTE_UNUSED( maxlen ); + return RR_Parse( data, token ); } - void SetContext( const char *context ) + /// Return a pointer to an IFileSystem we can use to read and process scripts. + virtual IFileSystem *GetFilesystem() { - delete[] m_szContext; - m_szContext = CopyString( context ); + return filesystem; } - const char *GetContext( void ) const { return m_szContext; } - - bool IsEnabled() const { return m_bEnabled; } - void Disable() { m_bEnabled = false; } - bool IsMatchOnce() const { return m_bMatchOnce; } - bool IsApplyContextToWorld() const { return m_bApplyContextToWorld; } - - // Indices into underlying criteria and response dictionaries - CUtlVector< unsigned short > m_Criteria; - CUtlVector< unsigned short> m_Responses; - - char *m_szContext; - bool m_bApplyContextToWorld : 1; - - bool m_bMatchOnce : 1; - bool m_bEnabled : 1; -}; -#pragma pack() - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -abstract_class CResponseSystem : public IResponseSystem -{ -public: - CResponseSystem(); - ~CResponseSystem(); - - // IResponseSystem - virtual bool FindBestResponse( const AI_CriteriaSet& set, AI_Response& response, IResponseFilter *pFilter = NULL ); - virtual void GetAllResponses( CUtlVector *pResponses ); - - virtual void Release() = 0; - - virtual void DumpRules(); - - virtual void Precache(); - - virtual void PrecacheResponses( bool bEnable ) + /// Return a pointer to an instance of an IUniformRandomStream + virtual IUniformRandomStream *GetRandomStream() { - m_bPrecache = bEnable; + return random; } - bool ShouldPrecache() { return m_bPrecache; } - bool IsCustomManagable() { return m_bCustomManagable; } - - void Clear(); - - void DumpDictionary( const char *pszName ); - -protected: - - virtual const char *GetScriptFile( void ) = 0; - void LoadRuleSet( const char *setname ); - - void ResetResponseGroups(); - - float LookForCriteria( const AI_CriteriaSet &criteriaSet, int iCriteria ); - float RecursiveLookForCriteria( const AI_CriteriaSet &criteriaSet, Criteria *pParent ); - -public: - - void CopyRuleFrom( Rule *pSrcRule, int iRule, CResponseSystem *pCustomSystem ); - void CopyCriteriaFrom( Rule *pSrcRule, Rule *pDstRule, CResponseSystem *pCustomSystem ); - void CopyResponsesFrom( Rule *pSrcRule, Rule *pDstRule, CResponseSystem *pCustomSystem ); - void CopyEnumerationsFrom( CResponseSystem *pCustomSystem ); - -//private: - - struct Enumeration - { - float value; - }; - - struct ResponseSearchResult - { - ResponseSearchResult() - { - group = NULL; - action = NULL; - } - - ResponseGroup *group; - Response *action; - }; - - inline bool ParseToken( void ) + /// Return a pointer to a tier0 ICommandLine + virtual ICommandLine *GetCommandLine() { - if ( m_bUnget ) - { - m_bUnget = false; - return true; - } - if ( m_ScriptStack.Count() <= 0 ) - { - Assert( 0 ); - return false; - } - - m_ScriptStack[ 0 ].currenttoken = engine->ParseFile( m_ScriptStack[ 0 ].currenttoken, token, sizeof( token ) ); - m_ScriptStack[ 0 ].tokencount++; - return m_ScriptStack[ 0 ].currenttoken != NULL ? true : false; + return CommandLine(); } - inline void Unget() + /// Emulates the server's UTIL_LoadFileForMe + virtual byte *LoadFileForMe( const char *filename, int *pLength ) { - m_bUnget = true; + return UTIL_LoadFileForMe( filename, pLength ); } - inline bool TokenWaiting( void ) + /// Emulates the server's UTIL_FreeFile + virtual void FreeFile( byte *buffer ) { - if ( m_ScriptStack.Count() <= 0 ) - { - Assert( 0 ); - return false; - } - - const char *p = m_ScriptStack[ 0 ].currenttoken; - - if ( !p ) - { - Error( "AI_ResponseSystem: Unxpected TokenWaiting() with NULL buffer in %p", m_ScriptStack[ 0 ].name ); - return false; - } - - - while ( *p && *p!='\n') - { - // Special handler for // comment blocks - if ( *p == '/' && *(p+1) == '/' ) - return false; - - if ( !isspace( *p ) || isalnum( *p ) ) - return true; - - p++; - } - - return false; + return UTIL_FreeFile( buffer ); } - - void ParseOneResponse( const char *responseGroupName, ResponseGroup& group ); - - void ParseInclude( CStringPool &includedFiles ); - void ParseResponse( void ); - void ParseCriterion( void ); - void ParseRule( void ); - void ParseEnumeration( void ); - - int ParseOneCriterion( const char *criterionName ); - - bool Compare( const char *setValue, Criteria *c, bool verbose = false ); - bool CompareUsingMatcher( const char *setValue, Matcher& m, bool verbose = false ); - void ComputeMatcher( Criteria *c, Matcher& matcher ); - void ResolveToken( Matcher& matcher, char *token, size_t bufsize, char const *rawtoken ); - float LookupEnumeration( const char *name, bool& found ); - - int FindBestMatchingRule( const AI_CriteriaSet& set, bool verbose ); - - float ScoreCriteriaAgainstRule( const AI_CriteriaSet& set, int irule, bool verbose = false ); - float RecursiveScoreSubcriteriaAgainstRule( const AI_CriteriaSet& set, Criteria *parent, bool& exclude, bool verbose /*=false*/ ); - float ScoreCriteriaAgainstRuleCriteria( const AI_CriteriaSet& set, int icriterion, bool& exclude, bool verbose = false ); - bool GetBestResponse( ResponseSearchResult& result, Rule *rule, bool verbose = false, IResponseFilter *pFilter = NULL ); - bool ResolveResponse( ResponseSearchResult& result, int depth, const char *name, bool verbose = false, IResponseFilter *pFilter = NULL ); - int SelectWeightedResponseFromResponseGroup( ResponseGroup *g, IResponseFilter *pFilter ); - void DescribeResponseGroup( ResponseGroup *group, int selected, int depth ); - void DebugPrint( int depth, const char *fmt, ... ); - - void LoadFromBuffer( const char *scriptfile, const char *buffer, CStringPool &includedFiles ); - - void GetCurrentScript( char *buf, size_t buflen ); - int GetCurrentToken() const; - void SetCurrentScript( const char *script ); - bool IsRootCommand(); - - void PushScript( const char *scriptfile, unsigned char *buffer ); - void PopScript(void); - - void ResponseWarning( const char *fmt, ... ); - - CUtlDict< ResponseGroup, short > m_Responses; - CUtlDict< Criteria, short > m_Criteria; - CUtlDict< Rule, short > m_Rules; - CUtlDict< Enumeration, short > m_Enumerations; - - char token[ 1204 ]; - - bool m_bUnget; - bool m_bPrecache; - bool m_bCustomManagable; - - struct ScriptEntry - { - unsigned char *buffer; - FileNameHandle_t name; - const char *currenttoken; - int tokencount; - }; +}; - CUtlVector< ScriptEntry > m_ScriptStack; +CResponseRulesToEngineInterface g_ResponseRulesEngineWrapper; +IEngineEmulator *IEngineEmulator::s_pSingleton = &g_ResponseRulesEngineWrapper; - friend class CDefaultResponseSystemSaveRestoreBlockHandler; - friend class CResponseSystemSaveRestoreOps; -}; -BEGIN_SIMPLE_DATADESC( Response ) +BEGIN_SIMPLE_DATADESC( ParserResponse ) // DEFINE_FIELD( type, FIELD_INTEGER ), // DEFINE_ARRAY( value, FIELD_CHARACTER ), // DEFINE_FIELD( weight, FIELD_FLOAT ), @@ -740,6 +200,7 @@ BEGIN_SIMPLE_DATADESC( Response ) // DEFINE_FIELD( last, FIELD_BOOLEAN ), END_DATADESC() + BEGIN_SIMPLE_DATADESC( ResponseGroup ) // DEFINE_FIELD( group, FIELD_UTLVECTOR ), // DEFINE_FIELD( rp, FIELD_EMBEDDED ), @@ -750,2109 +211,240 @@ BEGIN_SIMPLE_DATADESC( ResponseGroup ) // DEFINE_FIELD( m_bSequential, FIELD_BOOLEAN ), // DEFINE_FIELD( m_bNoRepeat, FIELD_BOOLEAN ), DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), - DEFINE_FIELD( m_nCurrentIndex, FIELD_CHARACTER ), -END_DATADESC() - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -CResponseSystem::CResponseSystem() -{ - token[0] = 0; - m_bUnget = false; - m_bPrecache = true; - m_bCustomManagable = false; -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -CResponseSystem::~CResponseSystem() -{ -} - -//----------------------------------------------------------------------------- -// Purpose: -// Output : char const -//----------------------------------------------------------------------------- -void CResponseSystem::GetCurrentScript( char *buf, size_t buflen ) -{ - Assert( buf ); - buf[ 0 ] = 0; - if ( m_ScriptStack.Count() <= 0 ) - return; - - if ( filesystem->String( m_ScriptStack[ 0 ].name, buf, buflen ) ) - { - return; - } - buf[ 0 ] = 0; -} - -void CResponseSystem::PushScript( const char *scriptfile, unsigned char *buffer ) -{ - ScriptEntry e; - e.name = filesystem->FindOrAddFileName( scriptfile ); - e.buffer = buffer; - e.currenttoken = (char *)e.buffer; - e.tokencount = 0; - m_ScriptStack.AddToHead( e ); -} - -void CResponseSystem::PopScript(void) -{ - Assert( m_ScriptStack.Count() >= 1 ); - if ( m_ScriptStack.Count() <= 0 ) - return; - - m_ScriptStack.Remove( 0 ); -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CResponseSystem::Clear() -{ - m_Responses.RemoveAll(); - m_Criteria.RemoveAll(); - m_Rules.RemoveAll(); - m_Enumerations.RemoveAll(); -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *name - -// found - -// Output : float -//----------------------------------------------------------------------------- -float CResponseSystem::LookupEnumeration( const char *name, bool& found ) -{ - int idx = m_Enumerations.Find( name ); - if ( idx == m_Enumerations.InvalidIndex() ) - { - found = false; - return 0.0f; - } - - - found = true; - return m_Enumerations[ idx ].value; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : matcher - -//----------------------------------------------------------------------------- -void CResponseSystem::ResolveToken( Matcher& matcher, char *token, size_t bufsize, char const *rawtoken ) -{ - if ( rawtoken[0] != '[' ) - { - Q_strncpy( token, rawtoken, bufsize ); - return; - } - - // Now lookup enumeration - bool found = false; - float f = LookupEnumeration( rawtoken, found ); - if ( !found ) - { - Q_strncpy( token, rawtoken, bufsize ); - ResponseWarning( "No such enumeration '%s'\n", token ); - return; - } - - Q_snprintf( token, bufsize, "%f", f ); -} - - -static bool AppearsToBeANumber( char const *token ) -{ - if ( atof( token ) != 0.0f ) - return true; - - char const *p = token; - while ( *p ) - { - if ( *p != '0' ) - return false; - - p++; - } - - return true; -} - -void CResponseSystem::ComputeMatcher( Criteria *c, Matcher& matcher ) -{ - const char *s = c->value; - if ( !s ) - { - matcher.valid = false; - return; - } - - const char *in = s; - - char token[ 128 ]; - char rawtoken[ 128 ]; - - token[ 0 ] = 0; - rawtoken[ 0 ] = 0; - - int n = 0; - - bool gt = false; - bool lt = false; - bool eq = false; - bool nt = false; - - bool done = false; - while ( !done ) - { - switch( *in ) - { - case '>': - { - gt = true; - Assert( !lt ); // Can't be both - } - break; - case '<': - { - lt = true; - Assert( !gt ); // Can't be both - } - break; - case '=': - { - eq = true; - } - break; - case ',': - case '\0': - { - rawtoken[ n ] = 0; - n = 0; - - // Convert raw token to real token in case token is an enumerated type specifier - ResolveToken( matcher, token, sizeof( token ), rawtoken ); - - // Fill in first data set - if ( gt ) - { - matcher.usemin = true; - matcher.minequals = eq; - matcher.minval = (float)atof( token ); - - matcher.isnumeric = true; - } - else if ( lt ) - { - matcher.usemax = true; - matcher.maxequals = eq; - matcher.maxval = (float)atof( token ); - - matcher.isnumeric = true; - } - else - { - if ( *in == ',' ) - { - // If there's a comma, this better have been a less than or a gt key - Assert( 0 ); - } - - matcher.notequal = nt; - - matcher.isnumeric = AppearsToBeANumber( token ); - } - - gt = lt = eq = nt = false; - - if ( !(*in) ) - { - done = true; - } - } - break; - case '!': - nt = true; - break; - default: - rawtoken[ n++ ] = *in; - break; - } - - in++; - } - - matcher.SetToken( token ); - matcher.SetRaw( rawtoken ); - matcher.valid = true; -} - -bool CResponseSystem::CompareUsingMatcher( const char *setValue, Matcher& m, bool verbose /*=false*/ ) -{ - if ( !m.valid ) - return false; - - float v = (float)atof( setValue ); - if ( setValue[0] == '[' ) - { - bool found = false; - v = LookupEnumeration( setValue, found ); - } - - int minmaxcount = 0; - - if ( m.usemin ) - { - if ( m.minequals ) - { - if ( v < m.minval ) - return false; - } - else - { - if ( v <= m.minval ) - return false; - } - - ++minmaxcount; - } - - if ( m.usemax ) - { - if ( m.maxequals ) - { - if ( v > m.maxval ) - return false; - } - else - { - if ( v >= m.maxval ) - return false; - } - - ++minmaxcount; - } - - // Had one or both criteria and met them - if ( minmaxcount >= 1 ) - { - return true; - } - - if ( m.notequal ) - { - if ( m.isnumeric ) - { - if ( v == (float)atof( m.GetToken() ) ) - return false; - } - else - { - if ( !Q_stricmp( setValue, m.GetToken() ) ) - return false; - } - - return true; - } - - if ( m.isnumeric ) - { - // If the setValue is "", the NPC doesn't have the key at all, - // in which case we shouldn't match "0". - if ( !setValue || !setValue[0] ) - return false; - - return v == (float)atof( m.GetToken() ); - } - - return !Q_stricmp( setValue, m.GetToken() ) ? true : false; -} - -bool CResponseSystem::Compare( const char *setValue, Criteria *c, bool verbose /*= false*/ ) -{ - Assert( c ); - Assert( setValue ); - - bool bret = CompareUsingMatcher( setValue, c->matcher, verbose ); - - if ( verbose ) - { - DevMsg( "'%20s' vs. '%20s' = ", setValue, c->value ); - - { - //DevMsg( "\n" ); - //m.Describe(); - } - } - return bret; -} - -float CResponseSystem::RecursiveScoreSubcriteriaAgainstRule( const AI_CriteriaSet& set, Criteria *parent, bool& exclude, bool verbose /*=false*/ ) -{ - float score = 0.0f; - int subcount = parent->subcriteria.Count(); - for ( int i = 0; i < subcount; i++ ) - { - int icriterion = parent->subcriteria[ i ]; - - bool excludesubrule = false; - if (verbose) - { - DevMsg( "\n" ); - } - score += ScoreCriteriaAgainstRuleCriteria( set, icriterion, excludesubrule, verbose ); - } - - exclude = ( parent->required && score == 0.0f ) ? true : false; - - return score * parent->weight.GetFloat(); -} - -float CResponseSystem::RecursiveLookForCriteria( const AI_CriteriaSet &criteriaSet, Criteria *pParent ) -{ - float flScore = 0.0f; - int nSubCount = pParent->subcriteria.Count(); - for ( int iSub = 0; iSub < nSubCount; ++iSub ) - { - int iCriteria = pParent->subcriteria[iSub]; - flScore += LookForCriteria( criteriaSet, iCriteria ); - } - - return flScore; -} - -float CResponseSystem::LookForCriteria( const AI_CriteriaSet &criteriaSet, int iCriteria ) -{ - Criteria *pCriteria = &m_Criteria[iCriteria]; - if ( pCriteria->IsSubCriteriaType() ) - { - return RecursiveLookForCriteria( criteriaSet, pCriteria ); - } - - int iIndex = criteriaSet.FindCriterionIndex( pCriteria->name ); - if ( iIndex == -1 ) - return 0.0f; - - Assert( criteriaSet.GetValue( iIndex ) ); - if ( Q_stricmp( criteriaSet.GetValue( iIndex ), pCriteria->value ) ) - return 0.0f; - - return 1.0f; -} - -float CResponseSystem::ScoreCriteriaAgainstRuleCriteria( const AI_CriteriaSet& set, int icriterion, bool& exclude, bool verbose /*=false*/ ) -{ - Criteria *c = &m_Criteria[ icriterion ]; - - if ( c->IsSubCriteriaType() ) - { - return RecursiveScoreSubcriteriaAgainstRule( set, c, exclude, verbose ); - } - - if ( verbose ) - { - DevMsg( " criterion '%25s':'%15s' ", m_Criteria.GetElementName( icriterion ), c->name ); - } - - exclude = false; - - float score = 0.0f; - - const char *actualValue = ""; - - int found = set.FindCriterionIndex( c->name ); - if ( found != -1 ) - { - actualValue = set.GetValue( found ); - if ( !actualValue ) - { - Assert( 0 ); - return score; - } - } - - Assert( actualValue ); - - if ( Compare( actualValue, c, verbose ) ) - { - float w = set.GetWeight( found ); - score = w * c->weight.GetFloat(); - - if ( verbose ) - { - DevMsg( "matched, weight %4.2f (s %4.2f x c %4.2f)", - score, w, c->weight.GetFloat() ); - } - } - else - { - if ( c->required ) - { - exclude = true; - if ( verbose ) - { - DevMsg( "failed (+exclude rule)" ); - } - } - else - { - if ( verbose ) - { - DevMsg( "failed" ); - } - } - } - - return score; -} - -float CResponseSystem::ScoreCriteriaAgainstRule( const AI_CriteriaSet& set, int irule, bool verbose /*=false*/ ) -{ - Rule *rule = &m_Rules[ irule ]; - float score = 0.0f; - - bool bBeingWatched = false; - - // See if we're trying to debug this rule - const char *pszText = rr_debugrule.GetString(); - if ( pszText && pszText[0] && !Q_stricmp( pszText, m_Rules.GetElementName( irule ) ) ) - { - bBeingWatched = true; - } - - if ( !rule->IsEnabled() ) - { - if ( bBeingWatched ) - { - DevMsg("Rule '%s' is disabled.\n", m_Rules.GetElementName( irule ) ); - } - return 0.0f; - } - - if ( bBeingWatched ) - { - verbose = true; - } - - if ( verbose ) - { - DevMsg( "Scoring rule '%s' (%i)\n{\n", m_Rules.GetElementName( irule ), irule+1 ); - } - - // Iterate set criteria - int count = rule->m_Criteria.Count(); - int i; - for ( i = 0; i < count; i++ ) - { - int icriterion = rule->m_Criteria[ i ]; - - bool exclude = false; - score += ScoreCriteriaAgainstRuleCriteria( set, icriterion, exclude, verbose ); - - if ( verbose ) - { - DevMsg( ", score %4.2f\n", score ); - } - - if ( exclude ) - { - score = 0.0f; - break; - } - } - - if ( verbose ) - { - DevMsg( "}\n" ); - } - - return score; -} - -void CResponseSystem::DebugPrint( int depth, const char *fmt, ... ) -{ - int indentchars = 3 * depth; - char *indent = (char *)_alloca( indentchars + 1); - indent[ indentchars ] = 0; - while ( --indentchars >= 0 ) - { - indent[ indentchars ] = ' '; - } - - // Dump text to debugging console. - va_list argptr; - char szText[1024]; - - va_start (argptr, fmt); - Q_vsnprintf (szText, sizeof( szText ), fmt, argptr); - va_end (argptr); - - DevMsg( "%s%s", indent, szText ); -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CResponseSystem::ResetResponseGroups() -{ - int i; - int c = m_Responses.Count(); - for ( i = 0; i < c; i++ ) - { - m_Responses[ i ].Reset(); - } -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *g - -// Output : int -//----------------------------------------------------------------------------- -int CResponseSystem::SelectWeightedResponseFromResponseGroup( ResponseGroup *g, IResponseFilter *pFilter ) -{ - int c = g->group.Count(); - if ( !c ) - { - Assert( !"Expecting response group with >= 1 elements" ); - return -1; - } - - int i; - - // Fake depletion of unavailable choices - CUtlVector fakedDepletes; - if ( pFilter && g->ShouldCheckRepeats() ) - { - for ( i = 0; i < c; i++ ) - { - Response *r = &g->group[ i ]; - if ( r->depletioncount != g->GetDepletionCount() && !pFilter->IsValidResponse( r->GetType(), r->value ) ) - { - fakedDepletes.AddToTail( i ); - g->MarkResponseUsed( i ); - } - } - } - - if ( !g->HasUndepletedChoices() ) - { - g->ResetDepletionCount(); - - if ( pFilter && g->ShouldCheckRepeats() ) - { - fakedDepletes.RemoveAll(); - for ( i = 0; i < c; i++ ) - { - Response *r = &g->group[ i ]; - if ( !pFilter->IsValidResponse( r->GetType(), r->value ) ) - { - fakedDepletes.AddToTail( i ); - g->MarkResponseUsed( i ); - } - } - } - - if ( !g->HasUndepletedChoices() ) - return -1; - - // Disable the group if we looped through all the way - if ( g->IsNoRepeat() ) - { - g->SetEnabled( false ); - return -1; - } - } - - bool checkrepeats = g->ShouldCheckRepeats(); - int depletioncount = g->GetDepletionCount(); - - float totalweight = 0.0f; - int slot = -1; - - if ( checkrepeats ) - { - int check= -1; - // Snag the first slot right away - if ( g->HasUndepletedFirst( check ) && check != -1 ) - { - slot = check; - } - - if ( slot == -1 && g->HasUndepletedLast( check ) && check != -1 ) - { - // If this is the only undepleted one, use it now - for ( i = 0; i < c; i++ ) - { - Response *r = &g->group[ i ]; - if ( checkrepeats && - ( r->depletioncount == depletioncount ) ) - { - continue; - } - - if ( r->last ) - { - Assert( i == check ); - continue; - } - - // There's still another undepleted entry - break; - } - - // No more undepleted so use the r->last slot - if ( i >= c ) - { - slot = check; - } - } - } - - if ( slot == -1 ) - { - for ( i = 0; i < c; i++ ) - { - Response *r = &g->group[ i ]; - if ( checkrepeats && - ( r->depletioncount == depletioncount ) ) - { - continue; - } - - // Always skip last entry here since we will deal with it above - if ( checkrepeats && r->last ) - continue; - - int prevSlot = slot; - - if ( !totalweight ) - { - slot = i; - } - - // Always assume very first slot will match - totalweight += r->weight.GetFloat(); - if ( !totalweight || random->RandomFloat(0,totalweight) < r->weight.GetFloat() ) - { - slot = i; - } - - if ( !checkrepeats && slot != prevSlot && pFilter && !pFilter->IsValidResponse( r->GetType(), r->value ) ) - { - slot = prevSlot; - totalweight -= r->weight.GetFloat(); - } - } - } - - if ( slot != -1 ) - g->MarkResponseUsed( slot ); - - // Revert fake depletion of unavailable choices - if ( pFilter && g->ShouldCheckRepeats() ) - { - for ( i = 0; i < fakedDepletes.Count(); i++ ) - { - g->group[ fakedDepletes[ i ] ].depletioncount = 0;; - } - } - - return slot; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : searchResult - -// depth - -// *name - -// verbose - -// Output : Returns true on success, false on failure. -//----------------------------------------------------------------------------- -bool CResponseSystem::ResolveResponse( ResponseSearchResult& searchResult, int depth, const char *name, bool verbose /*= false*/, IResponseFilter *pFilter ) -{ - int responseIndex = m_Responses.Find( name ); - if ( responseIndex == m_Responses.InvalidIndex() ) - return false; - - ResponseGroup *g = &m_Responses[ responseIndex ]; - // Group has been disabled - if ( !g->IsEnabled() ) - return false; - - int c = g->group.Count(); - if ( !c ) - return false; - - int idx = 0; - - if ( g->IsSequential() ) - { - // See if next index is valid - int initialIndex = g->GetCurrentIndex(); - bool bFoundValid = false; - - do - { - idx = g->GetCurrentIndex(); - g->SetCurrentIndex( idx + 1 ); - if ( idx >= c ) - { - if ( g->IsNoRepeat() ) - { - g->SetEnabled( false ); - return false; - } - idx = 0; - g->SetCurrentIndex( 0 ); - } - - if ( !pFilter || pFilter->IsValidResponse( g->group[idx].GetType(), g->group[idx].value ) ) - { - bFoundValid = true; - break; - } - - } while ( g->GetCurrentIndex() != initialIndex ); - - if ( !bFoundValid ) - return false; - } - else - { - idx = SelectWeightedResponseFromResponseGroup( g, pFilter ); - if ( idx < 0 ) - return false; - } - - if ( verbose ) - { - DebugPrint( depth, "%s\n", m_Responses.GetElementName( responseIndex ) ); - DebugPrint( depth, "{\n" ); - DescribeResponseGroup( g, idx, depth ); - } - - bool bret = true; - - Response *result = &g->group[ idx ]; - if ( result->type == RESPONSE_RESPONSE ) - { - // Recurse - bret = ResolveResponse( searchResult, depth + 1, result->value, verbose, pFilter ); - } - else - { - searchResult.action = result; - searchResult.group = g; - } - - if( verbose ) - { - DebugPrint( depth, "}\n" ); - } - - return bret; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *group - -// selected - -// depth - -//----------------------------------------------------------------------------- -void CResponseSystem::DescribeResponseGroup( ResponseGroup *group, int selected, int depth ) -{ - int c = group->group.Count(); - - for ( int i = 0; i < c ; i++ ) - { - Response *r = &group->group[ i ]; - DebugPrint( depth + 1, "%s%20s : %40s %5.3f\n", - i == selected ? "-> " : " ", - AI_Response::DescribeResponse( r->GetType() ), - r->value, - r->weight.GetFloat() ); - } -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *rule - -// Output : CResponseSystem::Response -//----------------------------------------------------------------------------- -bool CResponseSystem::GetBestResponse( ResponseSearchResult& searchResult, Rule *rule, bool verbose /*=false*/, IResponseFilter *pFilter ) -{ - int c = rule->m_Responses.Count(); - if ( !c ) - return false; - - int index = random->RandomInt( 0, c - 1 ); - int groupIndex = rule->m_Responses[ index ]; - - ResponseGroup *g = &m_Responses[ groupIndex ]; - - // Group has been disabled - if ( !g->IsEnabled() ) - return false; - - int count = g->group.Count(); - if ( !count ) - return false; - - int responseIndex = 0; - - if ( g->IsSequential() ) - { - // See if next index is valid - int initialIndex = g->GetCurrentIndex(); - bool bFoundValid = false; - - do - { - responseIndex = g->GetCurrentIndex(); - g->SetCurrentIndex( responseIndex + 1 ); - if ( responseIndex >= count ) - { - if ( g->IsNoRepeat() ) - { - g->SetEnabled( false ); - return false; - } - responseIndex = 0; - g->SetCurrentIndex( 0 ); - } - - if ( !pFilter || pFilter->IsValidResponse( g->group[responseIndex].GetType(), g->group[responseIndex].value ) ) - { - bFoundValid = true; - break; - } - - } while ( g->GetCurrentIndex() != initialIndex ); - - if ( !bFoundValid ) - return false; - } - else - { - responseIndex = SelectWeightedResponseFromResponseGroup( g, pFilter ); - if ( responseIndex < 0 ) - return false; - } - - - Response *r = &g->group[ responseIndex ]; - - int depth = 0; - - if ( verbose ) - { - DebugPrint( depth, "%s\n", m_Responses.GetElementName( groupIndex ) ); - DebugPrint( depth, "{\n" ); - - DescribeResponseGroup( g, responseIndex, depth ); - } - - bool bret = true; - - if ( r->type == RESPONSE_RESPONSE ) - { - bret = ResolveResponse( searchResult, depth + 1, r->value, verbose, pFilter ); - } - else - { - searchResult.action = r; - searchResult.group = g; - } - - if ( verbose ) - { - DebugPrint( depth, "}\n" ); - } - - return bret; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : set - -// verbose - -// Output : int -//----------------------------------------------------------------------------- -int CResponseSystem::FindBestMatchingRule( const AI_CriteriaSet& set, bool verbose ) -{ - CUtlVector< int > bestrules; - float bestscore = 0.001f; - - int c = m_Rules.Count(); - int i; - for ( i = 0; i < c; i++ ) - { - float score = ScoreCriteriaAgainstRule( set, i, verbose ); - // Check equals so that we keep track of all matching rules - if ( score >= bestscore ) - { - // Reset bucket - if( score != bestscore ) - { - bestscore = score; - bestrules.RemoveAll(); - } - - // Add to bucket - bestrules.AddToTail( i ); - } - } - - int bestCount = bestrules.Count(); - if ( bestCount <= 0 ) - return -1; - - if ( bestCount == 1 ) - return bestrules[ 0 ]; - - // Randomly pick one of the tied matching rules - int idx = random->RandomInt( 0, bestCount - 1 ); - if ( verbose ) - { - DevMsg( "Found %i matching rules, selecting slot %i\n", bestCount, idx ); - } - return bestrules[ idx ]; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : set - -// Output : AI_Response -//----------------------------------------------------------------------------- -bool CResponseSystem::FindBestResponse( const AI_CriteriaSet& set, AI_Response& response, IResponseFilter *pFilter ) -{ - bool valid = false; - - int iDbgResponse = rr_debugresponses.GetInt(); - bool showRules = ( iDbgResponse == 2 ); - bool showResult = ( iDbgResponse == 1 || iDbgResponse == 2 ); - - // Look for match. verbose mode used to be at level 2, but disabled because the writers don't actually care for that info. - int bestRule = FindBestMatchingRule( set, iDbgResponse == 3 ); - - ResponseType_t responseType = RESPONSE_NONE; - AI_ResponseParams rp; - - char ruleName[ 128 ]; - char responseName[ 128 ]; - const char *context; - bool bcontexttoworld; - ruleName[ 0 ] = 0; - responseName[ 0 ] = 0; - context = NULL; - bcontexttoworld = false; - if ( bestRule != -1 ) - { - Rule *r = &m_Rules[ bestRule ]; - - ResponseSearchResult result; - if ( GetBestResponse( result, r, showResult, pFilter ) ) - { - Q_strncpy( responseName, result.action->value, sizeof( responseName ) ); - responseType = result.action->GetType(); - rp = result.group->rp; - } - - Q_strncpy( ruleName, m_Rules.GetElementName( bestRule ), sizeof( ruleName ) ); - - // Disable the rule if it only allows for matching one time - if ( r->IsMatchOnce() ) - { - r->Disable(); - } - context = r->GetContext(); - bcontexttoworld = r->IsApplyContextToWorld(); - - valid = true; - } - - response.Init( responseType, responseName, set, rp, ruleName, context, bcontexttoworld ); - - if ( showResult ) - { - /* - // clipped -- chet doesn't really want this info - if ( valid ) - { - // Rescore the winner and dump to console - ScoreCriteriaAgainstRule( set, bestRule, true ); - } - */ - - - if ( valid || showRules ) - { - // Describe the response, too - response.Describe(); - } - } - - return valid; -} - -//----------------------------------------------------------------------------- -//----------------------------------------------------------------------------- -void CResponseSystem::GetAllResponses( CUtlVector *pResponses ) -{ - for ( int i = 0; i < (int)m_Responses.Count(); i++ ) - { - ResponseGroup &group = m_Responses[i]; - - for ( int j = 0; j < group.group.Count(); j++) - { - Response &response = group.group[j]; - if ( response.type != RESPONSE_RESPONSE ) - { - AI_Response *pResponse = new AI_Response; - pResponse->Init( response.GetType(), response.value, AI_CriteriaSet(), group.rp, NULL, NULL, false ); - pResponses->AddToTail(pResponse); - } - } - } -} - -static void TouchFile( char const *pchFileName ) -{ - filesystem->Size( pchFileName ); -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CResponseSystem::Precache() -{ - bool bTouchFiles = CommandLine()->FindParm( "-makereslists" ) != 0; - - // enumerate and mark all the scripts so we know they're referenced - for ( int i = 0; i < (int)m_Responses.Count(); i++ ) - { - ResponseGroup &group = m_Responses[i]; - - for ( int j = 0; j < group.group.Count(); j++) - { - Response &response = group.group[j]; - - switch ( response.type ) - { - default: - break; - case RESPONSE_SCENE: - { - // fixup $gender references - char file[_MAX_PATH]; - Q_strncpy( file, response.value, sizeof(file) ); - char *gender = strstr( file, "$gender" ); - if ( gender ) - { - // replace with male & female - const char *postGender = gender + strlen("$gender"); - *gender = 0; - char genderFile[_MAX_PATH]; - // male - Q_snprintf( genderFile, sizeof(genderFile), "%smale%s", file, postGender); - - PrecacheInstancedScene( genderFile ); - if ( bTouchFiles ) - { - TouchFile( genderFile ); - } - - Q_snprintf( genderFile, sizeof(genderFile), "%sfemale%s", file, postGender); - - PrecacheInstancedScene( genderFile ); - if ( bTouchFiles ) - { - TouchFile( genderFile ); - } - } - else - { - PrecacheInstancedScene( file ); - if ( bTouchFiles ) - { - TouchFile( file ); - } - } - } - break; - case RESPONSE_SPEAK: - { - CBaseEntity::PrecacheScriptSound( response.value ); - } - break; - } - } - } -} - -void CResponseSystem::ParseInclude( CStringPool &includedFiles ) -{ - char includefile[ 256 ]; - ParseToken(); - Q_snprintf( includefile, sizeof( includefile ), "scripts/%s", token ); - - // check if the file is already included - if ( includedFiles.Find( includefile ) != NULL ) - { - return; - } - - MEM_ALLOC_CREDIT(); - - // Try and load it - CUtlBuffer buf; - if ( !filesystem->ReadFile( includefile, "GAME", buf ) ) - { - DevMsg( "Unable to load #included script %s\n", includefile ); - return; - } - - LoadFromBuffer( includefile, (const char *)buf.PeekGet(), includedFiles ); -} - -void CResponseSystem::LoadFromBuffer( const char *scriptfile, const char *buffer, CStringPool &includedFiles ) -{ - includedFiles.Allocate( scriptfile ); - PushScript( scriptfile, (unsigned char * )buffer ); - - if( rr_dumpresponses.GetBool() ) - { - DevMsg("Reading: %s\n", scriptfile ); - } - - while ( 1 ) - { - ParseToken(); - if ( !token[0] ) - { - break; - } - - if ( !Q_stricmp( token, "#include" ) ) - { - ParseInclude( includedFiles ); - } - else if ( !Q_stricmp( token, "response" ) ) - { - ParseResponse(); - } - else if ( !Q_stricmp( token, "criterion" ) || - !Q_stricmp( token, "criteria" ) ) - { - ParseCriterion(); - } - else if ( !Q_stricmp( token, "rule" ) ) - { - ParseRule(); - } - else if ( !Q_stricmp( token, "enumeration" ) ) - { - ParseEnumeration(); - } - else - { - int byteoffset = m_ScriptStack[ 0 ].currenttoken - (const char *)m_ScriptStack[ 0 ].buffer; - - Error( "CResponseSystem::LoadFromBuffer: Unknown entry type '%s', expecting 'response', 'criterion', 'enumeration' or 'rules' in file %s(offset:%i)\n", - token, scriptfile, byteoffset ); - break; - } - } - - if ( m_ScriptStack.Count() == 1 ) - { - char cur[ 256 ]; - GetCurrentScript( cur, sizeof( cur ) ); - DevMsg( 1, "CResponseSystem: %s (%i rules, %i criteria, and %i responses)\n", - cur, m_Rules.Count(), m_Criteria.Count(), m_Responses.Count() ); - - if( rr_dumpresponses.GetBool() ) - { - DumpRules(); - } - } - - PopScript(); -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CResponseSystem::LoadRuleSet( const char *basescript ) -{ - int length = 0; - unsigned char *buffer = (unsigned char *)UTIL_LoadFileForMe( basescript, &length ); - if ( length <= 0 || !buffer ) - { - DevMsg( 1, "CResponseSystem: failed to load %s\n", basescript ); - return; - } - - CStringPool includedFiles; - - LoadFromBuffer( basescript, (const char *)buffer, includedFiles ); - - UTIL_FreeFile( buffer ); - - Assert( m_ScriptStack.Count() == 0 ); -} - -static ResponseType_t ComputeResponseType( const char *s ) -{ - if ( !Q_stricmp( s, "scene" ) ) - { - return RESPONSE_SCENE; - } - else if ( !Q_stricmp( s, "sentence" ) ) - { - return RESPONSE_SENTENCE; - } - else if ( !Q_stricmp( s, "speak" ) ) - { - return RESPONSE_SPEAK; - } - else if ( !Q_stricmp( s, "response" ) ) - { - return RESPONSE_RESPONSE; - } - else if ( !Q_stricmp( s, "print" ) ) - { - return RESPONSE_PRINT; - } - - return RESPONSE_NONE; -} - -void CResponseSystem::ParseOneResponse( const char *responseGroupName, ResponseGroup& group ) -{ - Response newResponse; - newResponse.weight.SetFloat( 1.0f ); - AI_ResponseParams *rp = &group.rp; - - newResponse.type = ComputeResponseType( token ); - if ( RESPONSE_NONE == newResponse.type ) - { - ResponseWarning( "response entry '%s' with unknown response type '%s'\n", responseGroupName, token ); - return; - } - - ParseToken(); - newResponse.value = CopyString( token ); - - while ( TokenWaiting() ) - { - ParseToken(); - if ( !Q_stricmp( token, "weight" ) ) - { - ParseToken(); - newResponse.weight.SetFloat( (float)atof( token ) ); - continue; - } - - if ( !Q_stricmp( token, "predelay" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_DELAYBEFORESPEAK; - rp->predelay.FromInterval( ReadInterval( token ) ); - continue; - } - - if ( !Q_stricmp( token, "nodelay" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; - rp->delay.start = 0; - rp->delay.range = 0; - continue; - } - - if ( !Q_stricmp( token, "defaultdelay" ) ) - { - rp->flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; - rp->delay.start = AIS_DEF_MIN_DELAY; - rp->delay.range = ( AIS_DEF_MAX_DELAY - AIS_DEF_MIN_DELAY ); - continue; - } - - if ( !Q_stricmp( token, "delay" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; - rp->delay.FromInterval( ReadInterval( token ) ); - continue; - } - - if ( !Q_stricmp( token, "speakonce" ) ) - { - rp->flags |= AI_ResponseParams::RG_SPEAKONCE; - continue; - } - - if ( !Q_stricmp( token, "noscene" ) ) - { - rp->flags |= AI_ResponseParams::RG_DONT_USE_SCENE; - continue; - } - - if ( !Q_stricmp( token, "stop_on_nonidle" ) ) - { - rp->flags |= AI_ResponseParams::RG_STOP_ON_NONIDLE; - continue; - } - - if ( !Q_stricmp( token, "odds" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_ODDS; - rp->odds = clamp( atoi( token ), 0, 100 ); - continue; - } - - if ( !Q_stricmp( token, "respeakdelay" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_RESPEAKDELAY; - rp->respeakdelay.FromInterval( ReadInterval( token ) ); - continue; - } - - if ( !Q_stricmp( token, "weapondelay" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_WEAPONDELAY; - rp->weapondelay.FromInterval( ReadInterval( token ) ); - continue; - } - - if ( !Q_stricmp( token, "soundlevel" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_SOUNDLEVEL; - rp->soundlevel = (soundlevel_t)TextToSoundLevel( token ); - continue; - } - - if ( !Q_stricmp( token, "displayfirst" ) ) - { - newResponse.first = true; - group.m_bHasFirst = true; - continue; - } - - if ( !Q_stricmp( token, "displaylast" ) ) - { - newResponse.last = true; - group.m_bHasLast= true; - continue; - } - - ResponseWarning( "response entry '%s' with unknown command '%s'\n", responseGroupName, token ); - } - - group.group.AddToTail( newResponse ); -} - -//----------------------------------------------------------------------------- -// Purpose: -// Output : Returns true on success, false on failure. -//----------------------------------------------------------------------------- -bool CResponseSystem::IsRootCommand() -{ - if ( !Q_stricmp( token, "#include" ) ) - return true; - if ( !Q_stricmp( token, "response" ) ) - return true; - if ( !Q_stricmp( token, "enumeration" ) ) - return true; - if ( !Q_stricmp( token, "criteria" ) ) - return true; - if ( !Q_stricmp( token, "criterion" ) ) - return true; - if ( !Q_stricmp( token, "rule" ) ) - return true; - return false; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *kv - -//----------------------------------------------------------------------------- -void CResponseSystem::ParseResponse( void ) -{ - // Should have groupname at start - char responseGroupName[ 128 ]; - - ResponseGroup newGroup; - AI_ResponseParams *rp = &newGroup.rp; - - // Response Group Name - ParseToken(); - Q_strncpy( responseGroupName, token, sizeof( responseGroupName ) ); - - while ( 1 ) - { - ParseToken(); - - // Oops, part of next definition - if( IsRootCommand() ) - { - Unget(); - break; - } - - if ( !Q_stricmp( token, "{" ) ) - { - while ( 1 ) - { - ParseToken(); - if ( !Q_stricmp( token, "}" ) ) - break; - - if ( !Q_stricmp( token, "permitrepeats" ) ) - { - newGroup.m_bDepleteBeforeRepeat = false; - continue; - } - else if ( !Q_stricmp( token, "sequential" ) ) - { - newGroup.SetSequential( true ); - continue; - } - else if ( !Q_stricmp( token, "norepeat" ) ) - { - newGroup.SetNoRepeat( true ); - continue; - } - - ParseOneResponse( responseGroupName, newGroup ); - } - break; - } - - if ( !Q_stricmp( token, "predelay" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_DELAYBEFORESPEAK; - rp->predelay.FromInterval( ReadInterval( token ) ); - continue; - } - - if ( !Q_stricmp( token, "nodelay" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; - rp->delay.start = 0; - rp->delay.range = 0; - continue; - } - - if ( !Q_stricmp( token, "defaultdelay" ) ) - { - rp->flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; - rp->delay.start = AIS_DEF_MIN_DELAY; - rp->delay.range = ( AIS_DEF_MAX_DELAY - AIS_DEF_MIN_DELAY ); - continue; - } - - if ( !Q_stricmp( token, "delay" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; - rp->delay.FromInterval( ReadInterval( token ) ); - continue; - } - - if ( !Q_stricmp( token, "speakonce" ) ) - { - rp->flags |= AI_ResponseParams::RG_SPEAKONCE; - continue; - } - - if ( !Q_stricmp( token, "noscene" ) ) - { - rp->flags |= AI_ResponseParams::RG_DONT_USE_SCENE; - continue; - } - - if ( !Q_stricmp( token, "stop_on_nonidle" ) ) - { - rp->flags |= AI_ResponseParams::RG_STOP_ON_NONIDLE; - continue; - } - - if ( !Q_stricmp( token, "odds" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_ODDS; - rp->odds = clamp( atoi( token ), 0, 100 ); - continue; - } - - if ( !Q_stricmp( token, "respeakdelay" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_RESPEAKDELAY; - rp->respeakdelay.FromInterval( ReadInterval( token ) ); - continue; - } - - if ( !Q_stricmp( token, "weapondelay" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_WEAPONDELAY; - rp->weapondelay.FromInterval( ReadInterval( token ) ); - continue; - } - - if ( !Q_stricmp( token, "soundlevel" ) ) - { - ParseToken(); - rp->flags |= AI_ResponseParams::RG_SOUNDLEVEL; - rp->soundlevel = (soundlevel_t)TextToSoundLevel( token ); - continue; - } - - ParseOneResponse( responseGroupName, newGroup ); - } - - m_Responses.Insert( responseGroupName, newGroup ); -} - - -//----------------------------------------------------------------------------- -// Purpose: -// Input : *criterion - -//----------------------------------------------------------------------------- -int CResponseSystem::ParseOneCriterion( const char *criterionName ) -{ - char key[ 128 ]; - char value[ 128 ]; - - Criteria newCriterion; - - bool gotbody = false; - - while ( TokenWaiting() || !gotbody ) - { - ParseToken(); - - // Oops, part of next definition - if( IsRootCommand() ) - { - Unget(); - break; - } - - if ( !Q_stricmp( token, "{" ) ) - { - gotbody = true; - - while ( 1 ) - { - ParseToken(); - if ( !Q_stricmp( token, "}" ) ) - break; - - // Look up subcriteria index - int idx = m_Criteria.Find( token ); - if ( idx != m_Criteria.InvalidIndex() ) - { - newCriterion.subcriteria.AddToTail( idx ); - } - else - { - ResponseWarning( "Skipping unrecongized subcriterion '%s' in '%s'\n", token, criterionName ); - } - } - continue; - } - else if ( !Q_stricmp( token, "required" ) ) - { - newCriterion.required = true; - } - else if ( !Q_stricmp( token, "weight" ) ) - { - ParseToken(); - newCriterion.weight.SetFloat( (float)atof( token ) ); - } - else - { - Assert( newCriterion.subcriteria.Count() == 0 ); + DEFINE_FIELD( m_nCurrentIndex, FIELD_CHARACTER ), +END_DATADESC() - // Assume it's the math info for a non-subcriteria resposne - Q_strncpy( key, token, sizeof( key ) ); - ParseToken(); - Q_strncpy( value, token, sizeof( value ) ); - newCriterion.name = CopyString( key ); - newCriterion.value = CopyString( value ); +/// Add some game-specific code to the basic response system +/// (eg, the scene precacher, which requires the client and server +/// to work) - gotbody = true; - } - } +class CGameResponseSystem : public CResponseSystem +{ +public: + CGameResponseSystem(); - if ( !newCriterion.IsSubCriteriaType() ) + virtual void Precache(); + virtual void PrecacheResponses( bool bEnable ) { - ComputeMatcher( &newCriterion, newCriterion.matcher ); + m_bPrecache = bEnable; } + bool ShouldPrecache() { return m_bPrecache; } - if ( m_Criteria.Find( criterionName ) != m_Criteria.InvalidIndex() ) - { - ResponseWarning( "Multiple definitions for criteria '%s'\n", criterionName ); - return m_Criteria.InvalidIndex(); - } +protected: + bool m_bPrecache; +}; - int idx = m_Criteria.Insert( criterionName, newCriterion ); - return idx; -} //----------------------------------------------------------------------------- // Purpose: -// Input : *kv - //----------------------------------------------------------------------------- -void CResponseSystem::ParseCriterion( void ) -{ - // Should have groupname at start - char criterionName[ 128 ]; - ParseToken(); - Q_strncpy( criterionName, token, sizeof( criterionName ) ); - - ParseOneCriterion( criterionName ); -} +CGameResponseSystem::CGameResponseSystem() : m_bPrecache(true) +{}; //----------------------------------------------------------------------------- // Purpose: -// Input : *kv - //----------------------------------------------------------------------------- -void CResponseSystem::ParseEnumeration( void ) + +#if 0 +class CScenePrecacheSystem : public CAutoGameSystem { - char enumerationName[ 128 ]; - ParseToken(); - Q_strncpy( enumerationName, token, sizeof( enumerationName ) ); +public: + CScenePrecacheSystem() : CAutoGameSystem( "CScenePrecacheSystem" ), m_RepeatCounts( 0, 0, DefLessFunc( int ) ) + { + } - ParseToken(); - if ( Q_stricmp( token, "{" ) ) + // Level init, shutdown + virtual void LevelShutdownPreEntity() { - ResponseWarning( "Expecting '{' in enumeration '%s', got '%s'\n", enumerationName, token ); - return; + m_RepeatCounts.Purge(); } - while ( 1 ) + bool ShouldPrecache( char const *pszScene ) { - ParseToken(); - if ( !Q_stricmp( token, "}" ) ) - break; + int hash = HashStringCaselessConventional( pszScene ); - if ( Q_strlen( token ) <= 0 ) + int slot = m_RepeatCounts.Find( hash ); + if ( slot != m_RepeatCounts.InvalidIndex() ) { - ResponseWarning( "Expecting more tokens in enumeration '%s'\n", enumerationName ); - break; + m_RepeatCounts[ slot ]++; + return false; } - char key[ 128 ]; - - Q_strncpy( key, token, sizeof( key ) ); - ParseToken(); - float value = (float)atof( token ); - - char sz[ 128 ]; - Q_snprintf( sz, sizeof( sz ), "[%s::%s]", enumerationName, key ); - Q_strlower( sz ); + m_RepeatCounts.Insert( hash, 0 ); + return true; + } - Enumeration newEnum; - newEnum.value = value; +private: - if ( m_Enumerations.Find( sz ) == m_Enumerations.InvalidIndex() ) - { - m_Enumerations.Insert( sz, newEnum ); - } - /* - else - { - ResponseWarning( "Ignoring duplication enumeration '%s'\n", sz ); - } - */ - } -} + CUtlMap< int, int > m_RepeatCounts; +}; +static CScenePrecacheSystem g_ScenePrecacheSystem; //----------------------------------------------------------------------------- -// Purpose: -// Input : *kv - +// Purpose: Used for precaching instanced scenes +// Input : *pszScene - //----------------------------------------------------------------------------- -void CResponseSystem::ParseRule( void ) +void PrecacheInstancedScene( char const *pszScene ) { - static int instancedCriteria = 0; + static int nMakingReslists = -1; - char ruleName[ 128 ]; - ParseToken(); - Q_strncpy( ruleName, token, sizeof( ruleName ) ); + if ( !g_ScenePrecacheSystem.ShouldPrecache( pszScene ) ) + return; - ParseToken(); - if ( Q_stricmp( token, "{" ) ) + if ( nMakingReslists == -1 ) { - ResponseWarning( "Expecting '{' in rule '%s', got '%s'\n", ruleName, token ); - return; + nMakingReslists = CommandLine()->FindParm( "-makereslists" ) > 0 ? 1 : 0; } - // entries are "criteria", "response" or an in-line criteria to instance - Rule newRule; - - char sz[ 128 ]; - - bool validRule = true; - while ( 1 ) + if ( nMakingReslists == 1 ) { - ParseToken(); - if ( !Q_stricmp( token, "}" ) ) - { - break; - } - - if ( Q_strlen( token ) <= 0 ) - { - ResponseWarning( "Expecting more tokens in rule '%s'\n", ruleName ); - break; - } - - if ( !Q_stricmp( token, "matchonce" ) ) - { - newRule.m_bMatchOnce = true; - continue; - } - - if ( !Q_stricmp( token, "applyContextToWorld" ) ) - { - newRule.m_bApplyContextToWorld = true; - continue; - } - - if ( !Q_stricmp( token, "applyContext" ) ) - { - ParseToken(); - if ( newRule.GetContext() == NULL ) - { - newRule.SetContext( token ); - } - else - { - CFmtStrN<1024> newContext( "%s,%s", newRule.GetContext(), token ); - newRule.SetContext( newContext ); - } - continue; - } - - if ( !Q_stricmp( token, "response" ) ) - { - // Read them until we run out. - while ( TokenWaiting() ) - { - ParseToken(); - int idx = m_Responses.Find( token ); - if ( idx != m_Responses.InvalidIndex() ) - { - MEM_ALLOC_CREDIT(); - newRule.m_Responses.AddToTail( idx ); - } - else - { - validRule = false; - ResponseWarning( "No such response '%s' for rule '%s'\n", token, ruleName ); - } - } - continue; - } - - if ( !Q_stricmp( token, "criteria" ) || - !Q_stricmp( token, "criterion" ) ) - { - // Read them until we run out. - while ( TokenWaiting() ) - { - ParseToken(); - - int idx = m_Criteria.Find( token ); - if ( idx != m_Criteria.InvalidIndex() ) - { - MEM_ALLOC_CREDIT(); - newRule.m_Criteria.AddToTail( idx ); - } - else - { - validRule = false; - ResponseWarning( "No such criterion '%s' for rule '%s'\n", token, ruleName ); - } - } - continue; - } - - // It's an inline criteria, generate a name and parse it in - Q_snprintf( sz, sizeof( sz ), "[%s%03i]", ruleName, ++instancedCriteria ); - Unget(); - int idx = ParseOneCriterion( sz ); - if ( idx != m_Criteria.InvalidIndex() ) - { - newRule.m_Criteria.AddToTail( idx ); - } + // Just stat the file to add to reslist + g_pFullFileSystem->Size( pszScene ); } - if ( validRule ) + // verify existence, cache is pre-populated, should be there + SceneCachedData_t sceneData; + if ( !scenefilecache->GetSceneCachedData( pszScene, &sceneData ) ) { - m_Rules.Insert( ruleName, newRule ); + // Scenes are sloppy and don't always exist. + // A scene that is not in the pre-built cache image, but on disk, is a true error. + if ( IsX360() && ( g_pFullFileSystem->GetDVDMode() != DVDMODE_STRICT ) && g_pFullFileSystem->FileExists( pszScene, "GAME" ) ) + { + Warning( "PrecacheInstancedScene: Missing scene '%s' from scene image cache.\nRebuild scene image cache!\n", pszScene ); + } } else { - DevMsg( "Discarded rule %s\n", ruleName ); + for ( int i = 0; i < sceneData.numSounds; ++i ) + { + short stringId = scenefilecache->GetSceneCachedSound( sceneData.sceneId, i ); + CBaseEntity::PrecacheScriptSound( scenefilecache->GetSceneString( stringId ) ); + } } -} -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -int CResponseSystem::GetCurrentToken() const -{ - if ( m_ScriptStack.Count() <= 0 ) - return -1; - - return m_ScriptStack[ 0 ].tokencount; + g_pStringTableClientSideChoreoScenes->AddString( CBaseEntity::IsServer(), pszScene ); } - - -void CResponseSystem::ResponseWarning( const char *fmt, ... ) +#endif +static void TouchFile( char const *pchFileName ) { - va_list argptr; -#ifndef _XBOX - static char string[1024]; -#else - char string[1024]; -#endif - - va_start (argptr, fmt); - Q_vsnprintf(string, sizeof(string), fmt,argptr); - va_end (argptr); - - char cur[ 256 ]; - GetCurrentScript( cur, sizeof( cur ) ); - DevMsg( 1, "%s(token %i) : %s", cur, GetCurrentToken(), string ); + IEngineEmulator::Get()->GetFilesystem()->Size( pchFileName ); } -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CResponseSystem::CopyCriteriaFrom( Rule *pSrcRule, Rule *pDstRule, CResponseSystem *pCustomSystem ) +void CGameResponseSystem::Precache() { - // Add criteria from this rule to global list in custom response system. - int nCriteriaCount = pSrcRule->m_Criteria.Count(); - for ( int iCriteria = 0; iCriteria < nCriteriaCount; ++iCriteria ) - { - int iSrcIndex = pSrcRule->m_Criteria[iCriteria]; - Criteria *pSrcCriteria = &m_Criteria[iSrcIndex]; - if ( pSrcCriteria ) - { - int iIndex = pCustomSystem->m_Criteria.Find( m_Criteria.GetElementName( iSrcIndex ) ); - if ( iIndex != pCustomSystem->m_Criteria.InvalidIndex() ) - { - pDstRule->m_Criteria.AddToTail( iIndex ); - continue; - } + bool bTouchFiles = CommandLine()->FindParm( "-makereslists" ) != 0; - // Add the criteria. - Criteria dstCriteria; + // enumerate and mark all the scripts so we know they're referenced + for ( int i = 0; i < (int)m_Responses.Count(); i++ ) + { + ResponseGroup &group = m_Responses[i]; - dstCriteria.name = CopyString( pSrcCriteria->name ); - dstCriteria.value = CopyString( pSrcCriteria->value ); - dstCriteria.weight = pSrcCriteria->weight; - dstCriteria.required = pSrcCriteria->required; - dstCriteria.matcher = pSrcCriteria->matcher; + for ( int j = 0; j < group.group.Count(); j++) + { + ParserResponse &response = group.group[j]; - int nSubCriteriaCount = pSrcCriteria->subcriteria.Count(); - for ( int iSubCriteria = 0; iSubCriteria < nSubCriteriaCount; ++iSubCriteria ) + switch ( response.type ) { - int iSrcSubIndex = pSrcCriteria->subcriteria[iSubCriteria]; - Criteria *pSrcSubCriteria = &m_Criteria[iSrcSubIndex]; - if ( pSrcCriteria ) + default: + break; + case RESPONSE_SCENE: { - int iSubIndex = pCustomSystem->m_Criteria.Find( pSrcSubCriteria->value ); - if ( iSubIndex != pCustomSystem->m_Criteria.InvalidIndex() ) - continue; + // fixup $gender references + char file[_MAX_PATH]; + Q_strncpy( file, response.value, sizeof(file) ); + char *gender = strstr( file, "$gender" ); + if ( gender ) + { + // replace with male & female + const char *postGender = gender + strlen("$gender"); + *gender = 0; + char genderFile[_MAX_PATH]; + // male + Q_snprintf( genderFile, sizeof(genderFile), "%smale%s", file, postGender); - // Add the criteria. - Criteria dstSubCriteria; + PrecacheInstancedScene( genderFile ); + if ( bTouchFiles ) + { + TouchFile( genderFile ); + } - dstSubCriteria.name = CopyString( pSrcSubCriteria->name ); - dstSubCriteria.value = CopyString( pSrcSubCriteria->value ); - dstSubCriteria.weight = pSrcSubCriteria->weight; - dstSubCriteria.required = pSrcSubCriteria->required; - dstSubCriteria.matcher = pSrcSubCriteria->matcher; + Q_snprintf( genderFile, sizeof(genderFile), "%sfemale%s", file, postGender); - int iSubInsertIndex = pCustomSystem->m_Criteria.Insert( pSrcSubCriteria->value, dstSubCriteria ); - dstCriteria.subcriteria.AddToTail( iSubInsertIndex ); + PrecacheInstancedScene( genderFile ); + if ( bTouchFiles ) + { + TouchFile( genderFile ); + } + } + else + { + PrecacheInstancedScene( file ); + if ( bTouchFiles ) + { + TouchFile( file ); + } + } } - } - - int iInsertIndex = pCustomSystem->m_Criteria.Insert( m_Criteria.GetElementName( iSrcIndex ), dstCriteria ); - pDstRule->m_Criteria.AddToTail( iInsertIndex ); - } - } -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CResponseSystem::CopyResponsesFrom( Rule *pSrcRule, Rule *pDstRule, CResponseSystem *pCustomSystem ) -{ - // Add responses from this rule to global list in custom response system. - int nResponseGroupCount = pSrcRule->m_Responses.Count(); - for ( int iResponseGroup = 0; iResponseGroup < nResponseGroupCount; ++iResponseGroup ) - { - int iSrcResponseGroup = pSrcRule->m_Responses[iResponseGroup]; - ResponseGroup *pSrcResponseGroup = &m_Responses[iSrcResponseGroup]; - if ( pSrcResponseGroup ) - { - // Add response group. - ResponseGroup dstResponseGroup; - - dstResponseGroup.rp = pSrcResponseGroup->rp; - dstResponseGroup.m_bDepleteBeforeRepeat = pSrcResponseGroup->m_bDepleteBeforeRepeat; - dstResponseGroup.m_nDepletionCount = pSrcResponseGroup->m_nDepletionCount; - dstResponseGroup.m_bHasFirst = pSrcResponseGroup->m_bHasFirst; - dstResponseGroup.m_bHasLast = pSrcResponseGroup->m_bHasLast; - dstResponseGroup.m_bSequential = pSrcResponseGroup->m_bSequential; - dstResponseGroup.m_bNoRepeat = pSrcResponseGroup->m_bNoRepeat; - dstResponseGroup.m_bEnabled = pSrcResponseGroup->m_bEnabled; - dstResponseGroup.m_nCurrentIndex = pSrcResponseGroup->m_nCurrentIndex; - - int nSrcResponseCount = pSrcResponseGroup->group.Count(); - for ( int iResponse = 0; iResponse < nSrcResponseCount; ++iResponse ) - { - Response *pSrcResponse = &pSrcResponseGroup->group[iResponse]; - if ( pSrcResponse ) + break; + case RESPONSE_SPEAK: { - // Add Response - Response dstResponse; - - dstResponse.weight = pSrcResponse->weight; - dstResponse.type = pSrcResponse->type; - dstResponse.value = CopyString( pSrcResponse->value ); - dstResponse.depletioncount = pSrcResponse->depletioncount; - dstResponse.first = pSrcResponse->first; - dstResponse.last = pSrcResponse->last; - - dstResponseGroup.group.AddToTail( dstResponse ); + CBaseEntity::PrecacheScriptSound( response.value ); } + break; } - - int iInsertIndex = pCustomSystem->m_Responses.Insert( m_Responses.GetElementName( iSrcResponseGroup ), dstResponseGroup ); - pDstRule->m_Responses.AddToTail( iInsertIndex ); - } - } -} - -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CResponseSystem::CopyEnumerationsFrom( CResponseSystem *pCustomSystem ) -{ - int nEnumerationCount = m_Enumerations.Count(); - for ( int iEnumeration = 0; iEnumeration < nEnumerationCount; ++iEnumeration ) - { - Enumeration *pSrcEnumeration = &m_Enumerations[iEnumeration]; - if ( pSrcEnumeration ) - { - Enumeration dstEnumeration; - dstEnumeration.value = pSrcEnumeration->value; - pCustomSystem->m_Enumerations.Insert( m_Enumerations.GetElementName( iEnumeration ), dstEnumeration ); } } } -//----------------------------------------------------------------------------- -// Purpose: -//----------------------------------------------------------------------------- -void CResponseSystem::CopyRuleFrom( Rule *pSrcRule, int iRule, CResponseSystem *pCustomSystem ) -{ - // Verify data. - Assert( pSrcRule ); - Assert( pCustomSystem ); - if ( !pSrcRule || !pCustomSystem ) - return; - - // New rule - Rule dstRule; - - dstRule.SetContext( pSrcRule->GetContext() ); - dstRule.m_bMatchOnce = pSrcRule->m_bMatchOnce; - dstRule.m_bEnabled = pSrcRule->m_bEnabled; - dstRule.m_bApplyContextToWorld = pSrcRule->m_bApplyContextToWorld; - - // Copy off criteria. - CopyCriteriaFrom( pSrcRule, &dstRule, pCustomSystem ); - - // Copy off responses. - CopyResponsesFrom( pSrcRule, &dstRule, pCustomSystem ); - - // Copy off enumerations - Don't think we use these. -// CopyEnumerationsFrom( pCustomSystem ); - - // Add rule. - pCustomSystem->m_Rules.Insert( m_Rules.GetElementName( iRule ), dstRule ); -} //----------------------------------------------------------------------------- // Purpose: A special purpose response system associated with a custom entity //----------------------------------------------------------------------------- -class CInstancedResponseSystem : public CResponseSystem +class CInstancedResponseSystem : public CGameResponseSystem { - typedef CResponseSystem BaseClass; + typedef CGameResponseSystem BaseClass; public: CInstancedResponseSystem( const char *scriptfile ) : m_pszScriptFile( 0 ) - { - Assert( scriptfile ); - - int len = Q_strlen( scriptfile ) + 1; - m_pszScriptFile = new char[ len ]; - Assert( m_pszScriptFile ); - Q_strncpy( m_pszScriptFile, scriptfile, len ); - } - - ~CInstancedResponseSystem() - { - delete[] m_pszScriptFile; - } - virtual const char *GetScriptFile( void ) - { - Assert( m_pszScriptFile ); - return m_pszScriptFile; - } - - // CAutoGameSystem - virtual bool Init() - { - const char *basescript = GetScriptFile(); - LoadRuleSet( basescript ); - return true; - } - - virtual void LevelInitPostEntity() - { - ResetResponseGroups(); - } - - virtual void Release() - { - Clear(); - delete this; - } + { + Assert( scriptfile ); + + int len = Q_strlen( scriptfile ) + 1; + m_pszScriptFile = new char[ len ]; + Assert( m_pszScriptFile ); + Q_strncpy( m_pszScriptFile, scriptfile, len ); + } + + ~CInstancedResponseSystem() + { + delete[] m_pszScriptFile; + } + virtual const char *GetScriptFile( void ) + { + Assert( m_pszScriptFile ); + return m_pszScriptFile; + } + + // CAutoGameSystem + virtual bool Init() + { + const char *basescript = GetScriptFile(); + LoadRuleSet( basescript ); + return true; + } + + virtual void LevelInitPostEntity() + { + ResetResponseGroups(); + } + + virtual void Release() + { + Clear(); + delete this; + } private: char *m_pszScriptFile; @@ -2861,7 +453,7 @@ class CInstancedResponseSystem : public CResponseSystem //----------------------------------------------------------------------------- // Purpose: The default response system for expressive AIs //----------------------------------------------------------------------------- -class CDefaultResponseSystem : public CResponseSystem, public CAutoGameSystem +class CDefaultResponseSystem : public CGameResponseSystem, public CAutoGameSystem { typedef CAutoGameSystem BaseClass; @@ -2903,6 +495,7 @@ class CDefaultResponseSystem : public CResponseSystem, public CAutoGameSystem IResponseSystem *PrecacheCustomResponseSystem( const char *scriptfile ) { + COM_TimestampedLog( "PrecacheCustomResponseSystem %s - Start", scriptfile ); CInstancedResponseSystem *sys = ( CInstancedResponseSystem * )FindResponseSystem( scriptfile ); if ( !sys ) { @@ -2922,6 +515,8 @@ class CDefaultResponseSystem : public CResponseSystem, public CAutoGameSystem sys->Precache(); + COM_TimestampedLog( "PrecacheCustomResponseSystem %s - Finish", scriptfile ); + return ( IResponseSystem * )sys; } @@ -2963,6 +558,9 @@ class CDefaultResponseSystem : public CResponseSystem, public CAutoGameSystem } } + // precache sounds in case we added new ones + Precache(); + } private: @@ -2979,9 +577,10 @@ class CDefaultResponseSystem : public CResponseSystem, public CAutoGameSystem } CUtlDict< CInstancedResponseSystem *, int > m_InstancedSystems; + friend void CC_RR_DumpHashInfo( const CCommand &args ); }; -IResponseSystem *CDefaultResponseSystem::BuildCustomResponseSystemGivenCriteria( const char *pszBaseFile, const char *pszCustomName, AI_CriteriaSet &criteriaSet, float flCriteriaScore ) +ResponseRules::IResponseSystem *CDefaultResponseSystem::BuildCustomResponseSystemGivenCriteria( const char *pszBaseFile, const char *pszCustomName, AI_CriteriaSet &criteriaSet, float flCriteriaScore ) { // Create a instanced response system. CInstancedResponseSystem *pCustomSystem = new CInstancedResponseSystem( pszCustomName ); @@ -2993,10 +592,15 @@ IResponseSystem *CDefaultResponseSystem::BuildCustomResponseSystemGivenCriteria( pCustomSystem->Clear(); // Copy the relevant rules and data. + /* int nRuleCount = m_Rules.Count(); for ( int iRule = 0; iRule < nRuleCount; ++iRule ) + */ + for ( ResponseRulePartition::tIndex iIdx = m_RulePartitions.First() ; + m_RulePartitions.IsValid(iIdx) ; + iIdx = m_RulePartitions.Next( iIdx ) ) { - Rule *pRule = &m_Rules[iRule]; + Rule *pRule = &m_RulePartitions[iIdx]; if ( pRule ) { float flScore = 0.0f; @@ -3009,7 +613,7 @@ IResponseSystem *CDefaultResponseSystem::BuildCustomResponseSystemGivenCriteria( flScore += LookForCriteria( criteriaSet, iRuleCriteria ); if ( flScore >= flCriteriaScore ) { - CopyRuleFrom( pRule, iRule, pCustomSystem ); + CopyRuleFrom( pRule, iIdx, pCustomSystem ); break; } } @@ -3020,7 +624,7 @@ IResponseSystem *CDefaultResponseSystem::BuildCustomResponseSystemGivenCriteria( m_bCustomManagable = true; AddInstancedResponseSystem( pszCustomName, pCustomSystem ); -// pCustomSystem->DumpDictionary( pszCustomName ); + // pCustomSystem->DumpDictionary( pszCustomName ); return pCustomSystem; } @@ -3032,23 +636,16 @@ void CDefaultResponseSystem::DestroyCustomResponseSystems() static CDefaultResponseSystem defaultresponsesytem; -IResponseSystem *g_pResponseSystem = &defaultresponsesytem; +ResponseRules::IResponseSystem *g_pResponseSystem = &defaultresponsesytem; CON_COMMAND( rr_reloadresponsesystems, "Reload all response system scripts." ) { +#ifdef GAME_DLL if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; +#endif defaultresponsesytem.ReloadAllResponseSystems(); - -#if defined( TF_DLL ) || defined ( TF_CLASSIC ) - // This is kind of hacky, but I need to get it in for now! - if( g_pGameRules->IsMultiplayer() ) - { - CMultiplayRules *pMultiplayRules = static_cast( g_pGameRules ); - pMultiplayRules->InitCustomResponseRulesDicts(); - } -#endif } static short RESPONSESYSTEM_SAVE_RESTORE_VERSION = 1; @@ -3067,7 +664,7 @@ class CDefaultResponseSystemSaveRestoreBlockHandler : public CDefSaveRestoreBloc { pSave->WriteShort( &RESPONSESYSTEM_SAVE_RESTORE_VERSION ); } - + void ReadRestoreHeaders( IRestore *pRestore ) { // No reason why any future version shouldn't try to retain backward compatability. The default here is to not do so. @@ -3094,7 +691,7 @@ class CDefaultResponseSystemSaveRestoreBlockHandler : public CDefSaveRestoreBloc pSave->WriteShort( &groupCount ); for ( int j = 0; j < groupCount; ++j ) { - const Response *response = &group->group[ j ]; + const ParserResponse *response = &group->group[ j ]; pSave->StartBlock( "Response" ); pSave->WriteString( response->value ); pSave->WriteAll( response ); @@ -3145,7 +742,7 @@ class CDefaultResponseSystemSaveRestoreBlockHandler : public CDefSaveRestoreBloc int ri; for ( ri = 0; ri < group->group.Count(); ++ri ) { - Response *response = &group->group[ ri ]; + ParserResponse *response = &group->group[ ri ]; if ( !Q_stricmp( response->value, responsename ) ) { break; @@ -3154,7 +751,7 @@ class CDefaultResponseSystemSaveRestoreBlockHandler : public CDefSaveRestoreBloc if ( ri < group->group.Count() ) { - Response *response = &group->group[ ri ]; + ParserResponse *response = &group->group[ ri ]; pRestore->ReadAll( response ); } } @@ -3172,7 +769,7 @@ class CDefaultResponseSystemSaveRestoreBlockHandler : public CDefSaveRestoreBloc bool m_fDoLoad; } g_DefaultResponseSystemSaveRestoreBlockHandler; - + ISaveRestoreBlockHandler *GetDefaultResponseSystemSaveRestoreBlockHandler() { return &g_DefaultResponseSystemSaveRestoreBlockHandler; @@ -3197,7 +794,7 @@ class CResponseSystemSaveRestoreOps : public CDefSaveRestoreOps CResponseSystem *pRS = *(CResponseSystem **)fieldInfo.pField; if ( !pRS || pRS == &defaultresponsesytem ) return; - + int count = pRS->m_Responses.Count(); pSave->WriteInt( &count ); for ( int i = 0; i < count; ++i ) @@ -3212,7 +809,7 @@ class CResponseSystemSaveRestoreOps : public CDefSaveRestoreOps pSave->WriteShort( &groupCount ); for ( int j = 0; j < groupCount; ++j ) { - const Response *response = &group->group[ j ]; + const ParserResponse *response = &group->group[ j ]; pSave->StartBlock( "Response" ); pSave->WriteString( response->value ); pSave->WriteAll( response ); @@ -3222,7 +819,7 @@ class CResponseSystemSaveRestoreOps : public CDefSaveRestoreOps pSave->EndBlock(); } } - + virtual void Restore( const SaveRestoreFieldInfo_t &fieldInfo, IRestore *pRestore ) { CResponseSystem *pRS = *(CResponseSystem **)fieldInfo.pField; @@ -3262,7 +859,7 @@ class CResponseSystemSaveRestoreOps : public CDefSaveRestoreOps int ri; for ( ri = 0; ri < group->group.Count(); ++ri ) { - Response *response = &group->group[ ri ]; + ParserResponse *response = &group->group[ ri ]; if ( !Q_stricmp( response->value, responsename ) ) { break; @@ -3271,7 +868,7 @@ class CResponseSystemSaveRestoreOps : public CDefSaveRestoreOps if ( ri < group->group.Count() ) { - Response *response = &group->group[ ri ]; + ParserResponse *response = &group->group[ ri ]; pRestore->ReadAll( response ); } } @@ -3284,7 +881,7 @@ class CResponseSystemSaveRestoreOps : public CDefSaveRestoreOps pRestore->EndBlock(); } } - + } g_ResponseSystemSaveRestoreOps; ISaveRestoreOps *responseSystemSaveRestoreOps = &g_ResponseSystemSaveRestoreOps; @@ -3295,12 +892,12 @@ ISaveRestoreOps *responseSystemSaveRestoreOps = &g_ResponseSystemSaveRestoreOps; //----------------------------------------------------------------------------- bool CDefaultResponseSystem::Init() { -/* + /* Warning( "sizeof( Response ) == %d\n", sizeof( Response ) ); Warning( "sizeof( ResponseGroup ) == %d\n", sizeof( ResponseGroup ) ); Warning( "sizeof( Criteria ) == %d\n", sizeof( Criteria ) ); Warning( "sizeof( AI_ResponseParams ) == %d\n", sizeof( AI_ResponseParams ) ); -*/ + */ const char *basescript = GetScriptFile(); LoadRuleSet( basescript ); @@ -3322,12 +919,13 @@ void CDefaultResponseSystem::Shutdown() BaseClass::Shutdown(); } + //----------------------------------------------------------------------------- // Purpose: Instance a custom response system // Input : *scriptfile - // Output : IResponseSystem //----------------------------------------------------------------------------- -IResponseSystem *PrecacheCustomResponseSystem( const char *scriptfile ) +ResponseRules::IResponseSystem *PrecacheCustomResponseSystem( const char *scriptfile ) { return defaultresponsesytem.PrecacheCustomResponseSystem( scriptfile ); } @@ -3338,7 +936,7 @@ IResponseSystem *PrecacheCustomResponseSystem( const char *scriptfile ) // set - // Output : IResponseSystem //----------------------------------------------------------------------------- -IResponseSystem *BuildCustomResponseSystemGivenCriteria( const char *pszBaseFile, const char *pszCustomName, AI_CriteriaSet &criteriaSet, float flCriteriaScore ) +ResponseRules::IResponseSystem *BuildCustomResponseSystemGivenCriteria( const char *pszBaseFile, const char *pszCustomName, AI_CriteriaSet &criteriaSet, float flCriteriaScore ) { return defaultresponsesytem.BuildCustomResponseSystemGivenCriteria( pszBaseFile, pszCustomName, criteriaSet, flCriteriaScore ); } @@ -3350,55 +948,3 @@ void DestroyCustomResponseSystems() { defaultresponsesytem.DestroyCustomResponseSystems(); } - -//----------------------------------------------------------------------------- -//----------------------------------------------------------------------------- -void CResponseSystem::DumpRules() -{ - int c = m_Rules.Count(); - int i; - - for ( i = 0; i < c; i++ ) - { - Msg("%s\n", m_Rules.GetElementName( i ) ); - } -} - -//----------------------------------------------------------------------------- -//----------------------------------------------------------------------------- -void CResponseSystem::DumpDictionary( const char *pszName ) -{ - Msg( "\nDictionary: %s\n", pszName ); - - int nRuleCount = m_Rules.Count(); - for ( int iRule = 0; iRule < nRuleCount; ++iRule ) - { - Msg(" Rule %d: %s\n", iRule, m_Rules.GetElementName( iRule ) ); - - Rule *pRule = &m_Rules[iRule]; - - int nCriteriaCount = pRule->m_Criteria.Count(); - for( int iCriteria = 0; iCriteria < nCriteriaCount; ++iCriteria ) - { - int iRuleCriteria = pRule->m_Criteria[iCriteria]; - Criteria *pCriteria = &m_Criteria[iRuleCriteria]; - Msg( " Criteria %d: %s %s\n", iCriteria, pCriteria->name, pCriteria->value ); - } - - int nResponseGroupCount = pRule->m_Responses.Count(); - for ( int iResponseGroup = 0; iResponseGroup < nResponseGroupCount; ++iResponseGroup ) - { - int iRuleResponse = pRule->m_Responses[iResponseGroup]; - ResponseGroup *pResponseGroup = &m_Responses[iRuleResponse]; - - Msg( " ResponseGroup %d: %s\n", iResponseGroup, m_Responses.GetElementName( iRuleResponse ) ); - - int nResponseCount = pResponseGroup->group.Count(); - for ( int iResponse = 0; iResponse < nResponseCount; ++iResponse ) - { - Response *pResponse = &pResponseGroup->group[iResponse]; - Msg( " Response %d: %s\n", iResponse, pResponse->value ); - } - } - } -} diff --git a/src/game/server/AI_ResponseSystem.h b/src/game/server/AI_ResponseSystem.h index a7b3a7977..a558f79e7 100644 --- a/src/game/server/AI_ResponseSystem.h +++ b/src/game/server/AI_ResponseSystem.h @@ -1,4 +1,4 @@ -//========= Copyright Valve Corporation, All rights reserved. ============// +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // @@ -13,27 +13,14 @@ #pragma once #endif -#include "AI_Criteria.h" +#include "ai_criteria.h" +#include "../../public/responserules/response_types.h" -abstract_class IResponseFilter -{ -public: - virtual ~IResponseFilter(){} - virtual bool IsValidResponse( ResponseType_t type, const char *pszValue ) = 0; -}; +// using ResponseRules::IResponseFilter; +// using ResponseRules::IResponseSystem; -abstract_class IResponseSystem -{ -public: - virtual ~IResponseSystem() {} - - virtual bool FindBestResponse( const AI_CriteriaSet& set, AI_Response& response, IResponseFilter *pFilter = NULL ) = 0; - virtual void GetAllResponses( CUtlVector *pResponses ) = 0; - virtual void PrecacheResponses( bool bEnable ) = 0; -}; - -IResponseSystem *PrecacheCustomResponseSystem( const char *scriptfile ); -IResponseSystem *BuildCustomResponseSystemGivenCriteria( const char *pszBaseFile, const char *pszCustomName, AI_CriteriaSet &criteriaSet, float flCriteriaScore ); +ResponseRules::IResponseSystem *PrecacheCustomResponseSystem( const char *scriptfile ); +ResponseRules::IResponseSystem *BuildCustomResponseSystemGivenCriteria( const char *pszBaseFile, const char *pszCustomName, AI_CriteriaSet &criteriaSet, float flCriteriaScore ); void DestroyCustomResponseSystems(); class ISaveRestoreBlockHandler *GetDefaultResponseSystemSaveRestoreBlockHandler(); diff --git a/src/game/server/ai_basenpc.h b/src/game/server/ai_basenpc.h index 31c2a4ed6..7534f3676 100644 --- a/src/game/server/ai_basenpc.h +++ b/src/game/server/ai_basenpc.h @@ -64,7 +64,6 @@ class CBaseGrenade; class CBaseDoor; class CBasePropDoor; struct AI_Waypoint_t; -class AI_Response; class CBaseFilter; typedef CBitVec CAI_ScheduleBits; diff --git a/src/game/server/ai_behavior_lead.h b/src/game/server/ai_behavior_lead.h index d59a87d37..0bf4ad0a4 100644 --- a/src/game/server/ai_behavior_lead.h +++ b/src/game/server/ai_behavior_lead.h @@ -9,12 +9,13 @@ #include "simtimer.h" #include "ai_behavior.h" +#include "ai_speechconcept.h" #if defined( _WIN32 ) #pragma once #endif -typedef const char *AIConcept_t; +typedef CAI_Concept AIConcept_t; // Speak concepts #define TLK_LEAD_START "TLK_LEAD_START" diff --git a/src/game/server/ai_expresserfollowup.cpp b/src/game/server/ai_expresserfollowup.cpp new file mode 100644 index 000000000..d9722dec2 --- /dev/null +++ b/src/game/server/ai_expresserfollowup.cpp @@ -0,0 +1,464 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" + +#include "ai_speech.h" + +#include "game.h" +#include "eventqueue.h" +#include "ai_basenpc.h" +#include "basemultiplayerplayer.h" +#include "ai_baseactor.h" +#include "sceneentity.h" +//#include "flex_expresser.h" +/* +#include "engine/ienginesound.h" +#include "keyvalues.h" +#include "ai_criteria.h" +#include "isaverestore.h" +#include "sceneentity.h" +*/ + + + +// memdbgon must be the last include file in a .cpp file!!! +#include + +static const char *GetResponseName( CBaseEntity *pEnt ) +{ + Assert( pEnt ); + if ( pEnt == NULL ) + return ""; + return STRING( pEnt->GetEntityName() ); +} + +// This is a tiny helper function for below -- what I'd use a lambda for, usually +static void DispatchComeback( CAI_ExpresserWithFollowup *pExpress, CBaseEntity *pSpeaker, CBaseEntity *pRespondent, AI_ResponseFollowup &followup ) +{ + AssertMsg(pSpeaker != NULL, "Response expressor somehow got called with a NULL Outer.\n"); + if ( !pRespondent ) + { + return; + } + + float delay = followup.followup_delay; + if (pSpeaker == pRespondent && delay < 0) + { + Warning("Response rule with a 'self' target specified negative delay, which isn't legal because that would make someone talk over himself."); + delay = 0; + } + + // Msg( "%s: Dispatch comeback about %s to %s\n", pSpeaker->GetBotString(), g_pConceptManager->GetTopicName( handle ), pRespondent->GetBotString() ); + + // build an input event that we will use to force the bot to talk through the IO system + variant_t value; + // Don't send along null contexts + if (followup.followup_contexts && followup.followup_contexts[0] != '\0') + { + value.SetString( MAKE_STRING( followup.followup_contexts ) ); + g_EventQueue.AddEvent( pRespondent, "AddContext", value, delay - 0.01, pSpeaker, pSpeaker ); + } + + /* + value.SetString(MAKE_STRING(followup.followup_concept)); + g_EventQueue.AddEvent( pRespondent, "SpeakResponseConcept", value, delay , pSpeaker, pSpeaker ); + */ + + AI_CriteriaSet criteria; + + // add in the FROM context so dispatchee knows was from me + const char * RESTRICT pszSpeakerName = GetResponseName( pSpeaker ); + criteria.AppendCriteria( "From", pszSpeakerName ); + // if a SUBJECT criteria is missing, put it back in. + if ( criteria.FindCriterionIndex( "Subject" ) == -1 ) + { + criteria.AppendCriteria( "Subject", pszSpeakerName ); + } + + // add in any provided contexts from the parameters onto the ones stored in the followup + criteria.Merge( followup.followup_contexts ); + + // This is kludgy and needs to be fixed in class hierarchy, but for now, try to guess at the most likely + // kinds of targets and dispatch to them. + if (CBaseMultiplayerPlayer *pPlayer = dynamic_cast(pRespondent)) + { + pPlayer->Speak( followup.followup_concept, &criteria ); + } + + else if (CAI_BaseActor *pActor = dynamic_cast(pRespondent)) + { + pActor->Speak( followup.followup_concept, &criteria ); + } +} + +#if 0 +//----------------------------------------------------------------------------- +// Purpose: Placeholder for rules based response system +// Input : concept - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_ExpresserWithFollowup::Speak( AIConcept_t &concept, const char *modifiers /*= NULL*/, char *pszOutResponseChosen /* = NULL*/, size_t bufsize /* = 0 */, IRecipientFilter *filter /* = NULL */ ) +{ + AI_Response *result = SpeakFindResponse( concept, modifiers ); + if ( !result ) + { + return false; + } + + CNPC_CompanionBot *pBot = dynamic_cast(GetOuter()); + if ( pBot ) + { + pBot->SetConversationTopic( g_pConceptManager->GetTopic( handle ) ); + pBot->SetLastSpeaker( g_pConceptManager->GetSpeaker( handle ) ); + // Msg( "%s: Conversing about %s\n", pBot->GetBotString(), g_pConceptManager->GetTopicName( handle ) ); + } + + SpeechMsg( GetOuter(), "%s (%x) spoke %s (%f)\n", STRING(GetOuter()->GetEntityName()), GetOuter(), g_pConceptManager->GetConcept( handle ), gpGlobals->curtime ); + + bool spoke = SpeakDispatchResponse( handle, result, filter ); + if ( pszOutResponseChosen ) + { + result->GetResponse( pszOutResponseChosen, bufsize ); + } + + return spoke; +} +#endif + + +// Work out the character from the "subject" context. +// Right now, this is a simple find by entity name search. +// But you can define arbitrary subject names, like L4D does +// for "biker", "manager", etc. +static CBaseEntity *AscertainSpeechSubjectFromContext( AI_Response *response, AI_CriteriaSet &criteria, const char *pContextName ) +{ + const char *subject = criteria.GetValue( criteria.FindCriterionIndex( pContextName ) ); + if (subject) + { + + return gEntList.FindEntityByName( NULL, subject ); + + } + else + { + return NULL; + } +} + +// TODO: Currently uses awful stricmp. Use symbols! Once I know which ones we want, that is. +static CResponseQueue::CFollowupTargetSpec_t ResolveFollowupTargetToEntity( AIConcept_t &concept, AI_CriteriaSet &criteria, const char * RESTRICT szTarget, AI_Response * RESTRICT response = NULL ) +{ + + + + if ( Q_stricmp(szTarget, "self") == 0 ) + { + return CResponseQueue::CFollowupTargetSpec_t( kDRT_SPECIFIC, concept.GetSpeaker() ); + } + else if ( Q_stricmp(szTarget, "subject") == 0 ) + { + return CResponseQueue::CFollowupTargetSpec_t( AscertainSpeechSubjectFromContext( response, criteria, "Subject" ) ); + } + else if ( Q_stricmp(szTarget, "from") == 0 ) + { + return CResponseQueue::CFollowupTargetSpec_t( AscertainSpeechSubjectFromContext( response, criteria, "From" ) ); + } + else if ( Q_stricmp(szTarget, "any") == 0 ) + { + return CResponseQueue::CFollowupTargetSpec_t( kDRT_ANY, concept.GetSpeaker() ); + } + else if ( Q_stricmp(szTarget, "all") == 0 ) + { + return CResponseQueue::CFollowupTargetSpec_t( kDRT_ALL ); + } + + // last resort, try a named lookup + else if ( CBaseEntity *pSpecific = gEntList.FindEntityByName(NULL, szTarget) ) // it could be anything + { + return CResponseQueue::CFollowupTargetSpec_t( pSpecific ); + } + + Warning("Couldn't resolve response target %s\n", szTarget ); + return CResponseQueue::CFollowupTargetSpec_t(); // couldn't resolve. +} + + +// TODO: Currently uses awful stricmp. Use symbols! Once I know which ones we want, that is. +static CResponseQueue::CFollowupTargetSpec_t ResolveFollowupTargetToEntity( AIConcept_t &concept, AI_CriteriaSet &criteria, AI_Response * RESTRICT response, AI_ResponseFollowup * RESTRICT followup ) +{ + const char * RESTRICT szTarget = followup->followup_target; + const CResponseQueue::CFollowupTargetSpec_t INVALID; // default: invalid result + if ( szTarget == NULL ) + return INVALID; + else + return ResolveFollowupTargetToEntity( concept, criteria, szTarget, response ); +} + + +ConVar chet_debug_idle( "chet_debug_idle", "0", FCVAR_ARCHIVE, "If set one, many debug prints to help track down the TLK_IDLE issue. Set two for super verbose info" ); +// extern ConVar chet_debug_idle; +bool CAI_ExpresserWithFollowup::Speak( AIConcept_t &concept, const char *modifiers /*= NULL*/, char *pszOutResponseChosen /* = NULL*/, size_t bufsize /* = 0 */, IRecipientFilter *filter /* = NULL */ ) +{ + VPROF("CAI_Expresser::Speak"); + if ( IsSpeechGloballySuppressed() ) + { + return false; + } + + concept.SetSpeaker(GetOuter()); + AI_CriteriaSet criteria; + GatherCriteria(&criteria, concept, modifiers); + GetOuter()->ModifyOrAppendDerivedCriteria(criteria); + AI_Response result; + if ( !FindResponse( result, concept, &criteria ) ) + { + if (chet_debug_idle.GetBool()) + { + + const char *name = GetOuter()->GetDebugName(); + + Msg( "TLK_IDLE: %s did not FindResponse\n", name ); + } + return false; + } + else + { + if (chet_debug_idle.GetBool()) + { + + + const char *name = GetOuter()->GetDebugName(); + + Msg( "TLK_IDLE: %s SUCCESSFUL FindResponse\n", name ); + } + } + + SpeechMsg( GetOuter(), "%s (%x) spoke %s (%f)", STRING(GetOuter()->GetEntityName()), GetOuter(), (const char*)concept, gpGlobals->curtime ); + // Msg( "%s:%s to %s:%s\n", GetOuter()->GetDebugName(), concept.GetStringConcept(), criteria.GetValue(criteria.FindCriterionIndex("Subject")), pTarget ? pTarget->GetDebugName() : "none" ); + + bool spoke = SpeakDispatchResponse( concept, &result, &criteria, filter ); + if ( pszOutResponseChosen ) + { + result.GetResponse( pszOutResponseChosen, bufsize ); + } + + return spoke; +} + +extern ISoundEmitterSystemBase* soundemitterbase; + +static float GetSpeechDurationForResponse( const AI_Response * RESTRICT response, const char *szActorModel) +{ + switch (response->GetType()) + { + case ResponseRules::RESPONSE_SCENE: + { + char szScene[MAX_PATH]; + soundemitterbase->GenderExpandString(szActorModel, response->GetResponsePtr(), szScene, MAX_PATH); + return GetSceneSpeechDuration(szScene); + } + break; + default: + break; + } + + return 0.f; +} + +//----------------------------------------------------------------------------- +// Purpose: Dispatches the result +// Input : *response - +//----------------------------------------------------------------------------- +bool CAI_ExpresserWithFollowup::SpeakDispatchResponse( AIConcept_t &concept, AI_Response *response, AI_CriteriaSet *criteria, IRecipientFilter *filter ) +{ + // This gives the chance for the other bot to respond. + if ( !concept.GetSpeaker().IsValid() ) + { + concept.SetSpeaker(GetOuter()); + } + + bool bInterrupted = IsSpeaking(); + bool bSuc = CAI_Expresser::SpeakDispatchResponse( concept, response, criteria, filter ); + if (!bSuc) + { + return false; + } + + if ( bInterrupted ) + { + g_ResponseQueueManager.GetQueue()->RemoveSpeechQueuedFor( GetOuter() ); + } + + // Record my followup details so that I may defer its use til end of the speech + AI_ResponseFollowup * RESTRICT followup = response->GetParams()->m_pFollowup; + if ( followup ) + { + if ( followup->followup_entityiotarget && followup->followup_entityioinput ) + if ( criteria ) + { + CBaseEntity * RESTRICT pTarget = gEntList.FindEntityByName( NULL, followup->followup_entityiotarget ); + if ( pTarget ) + { + g_EventQueue.AddEvent( pTarget, followup->followup_entityioinput, variant_t(), followup->followup_entityiodelay, GetOuter(), GetOuter() ); + } + } + if ( followup->IsValid() ) + { + // 11th hour change: rather than trigger followups from the end of a VCD, + // instead fire it from the end of the last speech event in the VCD, because + // there's a multisecond facial relax delay built into the scene. + // The speech length is stored in the cache, so we can post the followup now. + if ( response->GetType() == ResponseRules::RESPONSE_SCENE && + followup->followup_delay >= 0 ) + { + float fTimeToLastSpeech = GetSpeechDurationForResponse( response, GetOuter()->GetModelName().ToCStr()); + // failsafe + if ( fTimeToLastSpeech > 0 ) + { + DispatchFollowupThroughQueue( followup->followup_concept, followup->followup_contexts, + ResolveFollowupTargetToEntity( concept, *criteria, response, followup ), + fTimeToLastSpeech + followup->followup_delay, GetOuter() ); + } + else // error + { + // old way, copied from "else" below + m_pPostponedFollowup = followup; + if ( criteria ) + m_followupTarget = ResolveFollowupTargetToEntity( concept, *criteria, response, m_pPostponedFollowup ); + else + { + AI_CriteriaSet tmpCriteria; + m_followupTarget = ResolveFollowupTargetToEntity( concept, tmpCriteria, response, m_pPostponedFollowup ); + } + } + } + else if ( followup->followup_delay < 0 ) + { + // a negative delay has a special meaning. Usually the comeback dispatches after + // the currently said line is finished; the delay is added to that, to provide a + // pause between when character A finishes speaking and B begins. + // A negative delay (-n) actually means "dispatch the comeback n seconds + // after I start talking". + // In this case we do not need to postpone the followup; we just throw it directly + // into the queue. + DispatchFollowupThroughQueue( followup->followup_concept, followup->followup_contexts, + ResolveFollowupTargetToEntity( concept, *criteria, response, followup ), + -followup->followup_delay, GetOuter() ); + } + else if ( response->GetType() == ResponseRules::RESPONSE_PRINT ) + { // zero-duration responses dispatch immediately via the queue (must be the queue bec. + // the m_pPostponedFollowup will never trigger) + DispatchFollowupThroughQueue( followup->followup_concept, followup->followup_contexts, + ResolveFollowupTargetToEntity( concept, *criteria, response, followup ), + followup->followup_delay, GetOuter() ); + } + else + { + // this is kind of a quick patch to immediately deal with the issue of null criteria + // (arose while branching to main) without replumbing a bunch of stuff -- to be fixed + // 5.13.08 egr + m_pPostponedFollowup = followup; + if ( criteria ) + m_followupTarget = ResolveFollowupTargetToEntity( concept, *criteria, response, m_pPostponedFollowup ); + else + { + AI_CriteriaSet tmpCriteria; + m_followupTarget = ResolveFollowupTargetToEntity( concept, tmpCriteria, response, m_pPostponedFollowup ); + } + } + } + } + + + return bSuc; +} + +// This is a gimmick used when a negative delay is specified in a followup, which is a shorthand +// for "this many seconds after the beginning of the line" rather than "this may seconds after the end +// of the line", eg to create a THEN rule when two characters talk over each other. +// It's static to avoid accidental use of the postponed followup/target members. +void CAI_ExpresserWithFollowup::DispatchFollowupThroughQueue( const AIConcept_t &concept, + const char * RESTRICT criteriaStr, + const CResponseQueue::CFollowupTargetSpec_t &target, + float delay, + CBaseEntity * RESTRICT pOuter + ) +{ + AI_CriteriaSet criteria; + // Don't add my own criteria! GatherCriteria( &criteria, followup.followup_concept, followup.followup_contexts ); + + criteria.AppendCriteria( "From", STRING( pOuter->GetEntityName() ) ); + + criteria.Merge( criteriaStr ); + g_ResponseQueueManager.GetQueue()->Add( concept, &criteria, gpGlobals->curtime + delay, target, pOuter ); +} + +//----------------------------------------------------------------------------- +// Purpose: Handles the new concept objects +//----------------------------------------------------------------------------- +void CAI_ExpresserWithFollowup::SpeakDispatchFollowup( AI_ResponseFollowup &followup ) +{ + if ( !m_followupTarget.IsValid() ) + return; + + // If a specific entity target is given, use the old pathway for now + if ( m_followupTarget.m_iTargetType == kDRT_SPECIFIC && followup.followup_delay == 0 ) + { + CBaseEntity *pTarget = m_followupTarget.m_hHandle.Get(); + if (!pTarget) + { + return; + } + DispatchComeback( this, GetOuter(), pTarget, followup ); + } + else + { + DispatchFollowupThroughQueue( followup.followup_concept, followup.followup_contexts, m_followupTarget, followup.followup_delay, GetOuter() ); + } + // clear out the followup member just in case. + m_pPostponedFollowup = NULL; + m_followupTarget.m_iTargetType = kDRT_MAX; +} + +void CAI_ExpresserWithFollowup::OnSpeechFinished() +{ + if (m_pPostponedFollowup && m_pPostponedFollowup->IsValid()) + { + return SpeakDispatchFollowup(*m_pPostponedFollowup); + } +} + + + + +void CC_RR_ForceConcept_f( const CCommand &args ) +{ + if ( args.ArgC() < 3 ) + { + Msg("USAGE: rr_forceconcept \"criteria1:value1,criteria2:value2,...\"\n"); + return; + } + + AI_CriteriaSet criteria; + if ( args.ArgC() >= 3 ) + { + const char *criteriastring = args[3]; + criteria.Merge( criteriastring ); + } + + AIConcept_t concept( args[2] ); + QueueSpeak( concept, ResolveFollowupTargetToEntity( concept, criteria, args[1] ), criteria ); +} + + +static ConCommand rr_forceconcept( "rr_forceconcept", CC_RR_ForceConcept_f, + "fire a response concept directly at a given character.\n" + "USAGE: rr_forceconcept \"criteria1:value1,criteria2:value2,...\"\n" + "criteria values are optional.\n" + + , FCVAR_CHEAT ); diff --git a/src/game/server/ai_playerally.cpp b/src/game/server/ai_playerally.cpp index a60969913..a777c0d3c 100644 --- a/src/game/server/ai_playerally.cpp +++ b/src/game/server/ai_playerally.cpp @@ -138,12 +138,17 @@ bool ConceptStringLessFunc( const string_t &lhs, const string_t &rhs ) return CaselessStringLessThan( STRING(lhs), STRING(rhs) ); } +bool ConceptInfoStringLessFunc(const AIConcept_t& lhs, const AIConcept_t& rhs) +{ + return CaselessStringLessThan(lhs.GetStringConcept(), rhs.GetStringConcept()); +} + //----------------------------------------------------------------------------- class CConceptInfoMap : public CUtlMap { public: CConceptInfoMap() : - CUtlMap( CaselessStringLessThan ) + CUtlMap(ConceptInfoStringLessFunc) { for ( int i = 0; i < ARRAYSIZE(g_ConceptInfos); i++ ) { @@ -575,7 +580,7 @@ void CAI_PlayerAlly::PrescheduleThink( void ) if ( SelectNonCombatSpeech( &selection ) ) { SetSpeechTarget( selection.hSpeechTarget ); - SpeakDispatchResponse( selection.concept.c_str(), selection.Response ); + SpeakDispatchResponse( selection.concept.c_str(), &selection.Response ); m_flNextIdleSpeechTime = gpGlobals->curtime + RandomFloat( 20,30 ); } else @@ -715,7 +720,7 @@ bool CAI_PlayerAlly::SelectInterjection() if ( SelectIdleSpeech( &selection ) ) { SetSpeechTarget( selection.hSpeechTarget ); - SpeakDispatchResponse( selection.concept.c_str(), selection.Response ); + SpeakDispatchResponse( selection.concept.c_str(), &selection.Response ); return true; } } @@ -764,10 +769,10 @@ void CAI_PlayerAlly::PostSpeakDispatchResponse( AIConcept_t concept, AI_Response { //#ifdef HL2_EPISODIC CAI_AllySpeechManager *pSpeechManager = GetAllySpeechManager(); - ConceptInfo_t *pConceptInfo = pSpeechManager->GetConceptInfo( concept ); + ConceptInfo_t *pConceptInfo = pSpeechManager->GetConceptInfo( concept.GetStringConcept()); if ( pConceptInfo && (pConceptInfo->flags & AICF_QUESTION) && GetSpeechTarget() ) { - bool bSaidHelloToNPC = !Q_strcmp(concept, "TLK_HELLO_NPC"); + bool bSaidHelloToNPC = !Q_strcmp(concept.GetStringConcept(), "TLK_HELLO_NPC"); float duration = GetExpresser()->GetSemaphoreAvailableTime(this) - gpGlobals->curtime; @@ -775,11 +780,11 @@ void CAI_PlayerAlly::PostSpeakDispatchResponse( AIConcept_t concept, AI_Response { if ( bSaidHelloToNPC ) { - Warning("Q&A: '%s' said Hello to '%s' (concept %s)\n", GetDebugName(), GetSpeechTarget()->GetDebugName(), concept ); + Warning("Q&A: '%s' said Hello to '%s' (concept %s)\n", GetDebugName(), GetSpeechTarget()->GetDebugName(), concept.GetStringConcept() ); } else { - Warning("Q&A: '%s' questioned '%s' (concept %s)\n", GetDebugName(), GetSpeechTarget()->GetDebugName(), concept ); + Warning("Q&A: '%s' questioned '%s' (concept %s)\n", GetDebugName(), GetSpeechTarget()->GetDebugName(), concept.GetStringConcept()); } NDebugOverlay::HorzArrow( GetAbsOrigin(), GetSpeechTarget()->GetAbsOrigin(), 8, 0, 255, 0, 64, true, duration ); } @@ -915,7 +920,7 @@ void CAI_PlayerAlly::AnswerQuestion( CAI_PlayerAlly *pQuestioner, int iQARandomN } SetSpeechTarget( selection.hSpeechTarget ); - SpeakDispatchResponse( selection.concept.c_str(), selection.Response ); + SpeakDispatchResponse( selection.concept.c_str(), &selection.Response ); // Prevent idle speech for a while DeferAllIdleSpeech( random->RandomFloat( TALKER_DEFER_IDLE_SPEAK_MIN, TALKER_DEFER_IDLE_SPEAK_MAX ), GetSpeechTarget()->MyNPCPointer() ); @@ -1042,7 +1047,7 @@ void CAI_PlayerAlly::StartTask( const Task_t *pTask ) case TASK_TALKER_SPEAK_PENDING: if ( !m_PendingConcept.empty() ) { - SpeakDispatchResponse( m_PendingConcept.c_str(), m_PendingResponse ); + SpeakDispatchResponse( m_PendingConcept.c_str(), &m_PendingResponse ); m_PendingConcept.erase(); TaskComplete(); } @@ -1764,7 +1769,7 @@ bool CAI_PlayerAlly::RespondedTo( const char *ResponseConcept, bool bForce, bool if ( bCancelScene ) RemoveActorFromScriptedScenes( this, false ); - return SpeakDispatchResponse( ResponseConcept, response ); + return SpeakDispatchResponse( ResponseConcept, &response ); } return false; diff --git a/src/game/server/ai_speech.cpp b/src/game/server/ai_speech.cpp index 7661eb9c2..f486f9cb6 100644 --- a/src/game/server/ai_speech.cpp +++ b/src/game/server/ai_speech.cpp @@ -1,4 +1,4 @@ -//========= Copyright Valve Corporation, All rights reserved. ============// +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // @@ -10,12 +10,14 @@ #include "ai_speech.h" #include "game.h" -#include "engine/IEngineSound.h" -#include "KeyValues.h" +#include "engine/ienginesound.h" +#include "keyvalues.h" #include "ai_basenpc.h" -#include "AI_Criteria.h" +#include "ai_criteria.h" #include "isaverestore.h" #include "sceneentity.h" +#include "ai_speechqueue.h" +#include "ai_squad.h" // memdbgon must be the last include file in a .cpp file!!! #include @@ -29,6 +31,8 @@ inline void SpeechMsg( ... ) {} #define DebuggingSpeech() (false) #endif +using namespace ResponseRules; + extern ConVar rr_debugresponses; //----------------------------------------------------------------------------- @@ -38,33 +42,21 @@ CAI_TimedSemaphore g_AIFoesTalkSemaphore; ConceptHistory_t::~ConceptHistory_t() { - delete response; - response = NULL; } ConceptHistory_t::ConceptHistory_t( const ConceptHistory_t& src ) { timeSpoken = src.timeSpoken; - response = NULL; - if ( src.response ) - { - response = new AI_Response( *src.response ); - } + m_response = src.m_response ; } ConceptHistory_t& ConceptHistory_t::operator =( const ConceptHistory_t& src ) { - if ( this != &src ) - { - timeSpoken = src.timeSpoken; + if ( this == &src ) + return *this; - delete response; - response = NULL; - if ( src.response ) - { - response = new AI_Response( *src.response ); - } - } + timeSpoken = src.timeSpoken; + m_response = src.m_response ; return *this; } @@ -88,18 +80,18 @@ class CConceptHistoriesDataOps : public CDefSaveRestoreOps pSave->StartBlock(); { + // Write element name pSave->WriteString( ch->GetElementName( i ) ); // Write data pSave->WriteAll( pHistory ); - // Write response blob - bool hasresponse = !!pHistory->response; + bool hasresponse = !pHistory->m_response.IsEmpty() ; pSave->WriteBool( &hasresponse ); if ( hasresponse ) { - pSave->WriteAll( pHistory->response ); + pSave->WriteAll( &pHistory->m_response ); } // TODO: Could blat out pHistory->criteria pointer here, if it's needed } @@ -117,7 +109,6 @@ class CConceptHistoriesDataOps : public CDefSaveRestoreOps { char conceptname[ 512 ]; conceptname[ 0 ] = 0; - ConceptHistory_t history; pRestore->StartBlock(); @@ -127,11 +118,16 @@ class CConceptHistoriesDataOps : public CDefSaveRestoreOps pRestore->ReadAll( &history ); bool hasresponse = false; + pRestore->ReadBool( &hasresponse ); if ( hasresponse ) { - history.response = new AI_Response(); - pRestore->ReadAll( history.response ); + history.m_response; + pRestore->ReadAll( &history.m_response ); + } + else + { + history.m_response.Invalidate(); } } @@ -150,7 +146,7 @@ class CConceptHistoriesDataOps : public CDefSaveRestoreOps } } } - + virtual void MakeEmpty( const SaveRestoreFieldInfo_t &fieldInfo ) { } @@ -164,6 +160,84 @@ class CConceptHistoriesDataOps : public CDefSaveRestoreOps CConceptHistoriesDataOps g_ConceptHistoriesSaveDataOps; +///////////////////////////////////////////////// +// context operators +RR::CApplyContextOperator RR::sm_OpCopy(0); // " +RR::CIncrementOperator RR::sm_OpIncrement(2); // "++" +RR::CDecrementOperator RR::sm_OpDecrement(2); // "--" +RR::CToggleOperator RR::sm_OpToggle(1); // "!" + +RR::CApplyContextOperator *RR::CApplyContextOperator::FindOperator( const char *pContextString ) +{ + if ( !pContextString || pContextString[0] == 0 ) + { + return &sm_OpCopy; + } + + if ( pContextString[0] == '+' && pContextString [1] == '+' && pContextString[2] != '\0' ) + { + return &sm_OpIncrement; + } + else if ( pContextString[0] == '-' && pContextString [1] == '-' && pContextString[2] != '\0' ) + { + return &sm_OpDecrement; + } + else if ( pContextString[0] == '!' ) + { + return &sm_OpToggle; + } + else + { + return &sm_OpCopy; + } +} + +// default is just copy +bool RR::CApplyContextOperator::Apply( const char *pOldValue, const char *pOperator, char *pNewValue, int pNewValBufSize ) +{ + Assert( pOperator && pNewValue && pNewValBufSize > 0 ); + Assert( m_nSkipChars == 0 ); + if ( pOperator ) + { + V_strncpy( pNewValue, pOperator, pNewValBufSize ); + } + else + { + *pNewValue = 0; + } + return true; +} + +bool RR::CIncrementOperator::Apply( const char *pOldValue, const char *pOperator, char *pNewValue, int pNewValBufSize ) +{ + Assert( pOperator[0] == '+' && pOperator[1] == '+' ); + // parse out the old value as a numeric + int nOld = pOldValue ? V_atoi(pOldValue) : 0; + int nInc = V_atoi( pOperator+m_nSkipChars ); + V_snprintf( pNewValue, pNewValBufSize, "%d", nOld+nInc ); + return true; +} + +bool RR::CDecrementOperator::Apply( const char *pOldValue, const char *pOperator, char *pNewValue, int pNewValBufSize ) +{ + Assert( pOperator[0] == '-' && pOperator[1] == '-' ); + // parse out the old value as a numeric + int nOld = pOldValue ? V_atoi(pOldValue) : 0; + int nInc = V_atoi( pOperator+m_nSkipChars ); + V_snprintf( pNewValue, pNewValBufSize, "%d", nOld-nInc ); + return true; +} + +bool RR::CToggleOperator::Apply( const char *pOldValue, const char *pOperator, char *pNewValue, int pNewValBufSize ) +{ + Assert( pOperator[0] == '!' ); + // parse out the old value as a numeric + int nOld = pOldValue ? V_atoi(pOldValue) : 0; + V_snprintf( pNewValue, pNewValBufSize, "%d", nOld ? 0 : 1 ); + return true; +} + + //----------------------------------------------------------------------------- // // CLASS: CAI_Expresser @@ -210,83 +284,186 @@ int CAI_Expresser::GetVoicePitch() const static int g_nExpressers; #endif +/* +inline bool ShouldBeInExpresserQueue( CBaseFlex *pOuter ) +{ + return true; // return IsTerrorPlayer( pOuter, TEAM_SURVIVOR ); +} +*/ + CAI_Expresser::CAI_Expresser( CBaseFlex *pOuter ) : m_pOuter( pOuter ), m_pSink( NULL ), m_flStopTalkTime( 0 ), - m_flLastTimeAcceptedSpeak( 0 ), m_flBlockedTalkTime( 0 ), m_flStopTalkTimeWithoutDelay( 0 ), - m_voicePitch( 100 ) + m_voicePitch( 100 ), + m_flLastTimeAcceptedSpeak( 0 ) { #ifdef DEBUG g_nExpressers++; #endif + if (m_pOuter) + { + // register me with the global expresser queue. + + // L4D: something a little ass backwards is happening here. We only want + // survivors to be in the queue. However, the team number isn't + // specified yet. So, we actually need to do this in the player's ChangeTeam. + g_ResponseQueueManager.GetQueue()->AddExpresserHost(m_pOuter); + + } } CAI_Expresser::~CAI_Expresser() { m_ConceptHistories.Purge(); - CAI_TimedSemaphore *pSemaphore = GetMySpeechSemaphore( GetOuter() ); - if ( pSemaphore ) + CBaseFlex *RESTRICT outer = GetOuter(); + if ( outer ) { - if ( pSemaphore->GetOwner() == GetOuter() ) - pSemaphore->Release(); + CAI_TimedSemaphore *pSemaphore = GetMySpeechSemaphore( outer ); + if ( pSemaphore ) + { + if ( pSemaphore->GetOwner() == outer ) + pSemaphore->Release(); #ifdef DEBUG - g_nExpressers--; - if ( g_nExpressers == 0 && pSemaphore->GetOwner() ) - DevMsg( 2, "Speech semaphore being held by non-talker entity\n" ); + g_nExpressers--; + if ( g_nExpressers == 0 && pSemaphore->GetOwner() ) + DevMsg( 2, "Speech semaphore being held by non-talker entity\n" ); #endif + } + + g_ResponseQueueManager.GetQueue()->RemoveExpresserHost(outer); } } -//----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_Expresser::TestAllResponses() { IResponseSystem *pResponseSystem = GetOuter()->GetResponseSystem(); if ( pResponseSystem ) { - CUtlVector responses; - + CUtlVector responses; pResponseSystem->GetAllResponses( &responses ); for ( int i = 0; i < responses.Count(); i++ ) { - const char *szResponse = responses[i]->GetResponsePtr(); + char response[ 256 ]; + responses[i].GetResponse( response, sizeof( response ) ); - Msg( "Response: %s\n", szResponse ); - SpeakDispatchResponse( "", *responses[i] ); + Msg( "Response: %s\n", response ); + AIConcept_t concept; + SpeakDispatchResponse( concept, &responses[i], NULL ); } } } +//----------------------------------------------------------------------------- +void CAI_Expresser::SetOuter( CBaseFlex *pOuter ) +{ + // if we're changing outers (which is a strange thing to do) + // unregister the old one from the queue. + if ( m_pOuter && ( m_pOuter != pOuter ) ) + { + AssertMsg2( false, "Expresser is switching its Outer from %s to %s. Why?", m_pOuter->GetDebugName(), pOuter->GetDebugName() ); + // unregister me with the global expresser queue + g_ResponseQueueManager.GetQueue()->RemoveExpresserHost(m_pOuter); + } + + m_pOuter = pOuter; +} + //----------------------------------------------------------------------------- static const int LEN_SPECIFIC_SCENE_MODIFIER = strlen( AI_SPECIFIC_SCENE_MODIFIER ); + +// This function appends "Global world" criteria that are always added to +// any character doing any match. This represents global concepts like weather, who's +// alive, etc. +static void ModifyOrAppendGlobalCriteria( AI_CriteriaSet * RESTRICT outputSet ) +{ + return; +} + + +void CAI_Expresser::GatherCriteria( AI_CriteriaSet * RESTRICT outputSet, const AIConcept_t &concept, const char * RESTRICT modifiers ) +{ + // Always include the concept name + outputSet->AppendCriteria( "concept", concept, CONCEPT_WEIGHT ); + +#if 1 + outputSet->Merge( modifiers ); +#else + // Always include any optional modifiers + if ( modifiers != NULL ) + { + char copy_modifiers[ 255 ]; + const char *pCopy; + char key[ 128 ] = { 0 }; + char value[ 128 ] = { 0 }; + + Q_strncpy( copy_modifiers, modifiers, sizeof( copy_modifiers ) ); + pCopy = copy_modifiers; + + while( pCopy ) + { + pCopy = SplitContext( pCopy, key, sizeof( key ), value, sizeof( value ), NULL, modifiers ); + + if( *key && *value ) + { + outputSet->AppendCriteria( key, value, CONCEPT_WEIGHT ); + } + } + } +#endif + + // include any global criteria + ModifyOrAppendGlobalCriteria( outputSet ); + + // Let our outer fill in most match criteria + GetOuter()->ModifyOrAppendCriteria( *outputSet ); + + // Append local player criteria to set, but not if this is a player doing the talking + if ( !GetOuter()->IsPlayer() ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + if( pPlayer ) + pPlayer->ModifyOrAppendPlayerCriteria( *outputSet ); + } +} + //----------------------------------------------------------------------------- // Purpose: Searches for a possible response // Input : concept - // NULL - // Output : AI_Response //----------------------------------------------------------------------------- -bool CAI_Expresser::SpeakFindResponse( AI_Response &outResponse, AIConcept_t concept, const char *modifiers /*= NULL*/ ) +// AI_Response *CAI_Expresser::SpeakFindResponse( AIConcept_t concept, const char *modifiers /*= NULL*/ ) +bool CAI_Expresser::FindResponse( AI_Response &outResponse, AIConcept_t &concept, AI_CriteriaSet *criteria ) { + VPROF("CAI_Expresser::FindResponse"); IResponseSystem *rs = GetOuter()->GetResponseSystem(); if ( !rs ) { Assert( !"No response system installed for CAI_Expresser::GetOuter()!!!" ); + return NULL; + } + + // if I'm dead, I can't possibly match dialog. + if ( !GetOuter()->IsAlive() ) + { return false; } +#if 0 // this is the old technique, where we always gathered criteria in this function AI_CriteriaSet set; // Always include the concept name set.AppendCriteria( "concept", concept, CONCEPT_WEIGHT ); -#ifndef HL2_LAZUL + // Always include any optional modifiers - if ( modifiers ) + if ( modifiers != NULL ) { char copy_modifiers[ 255 ]; const char *pCopy; @@ -298,7 +475,7 @@ bool CAI_Expresser::SpeakFindResponse( AI_Response &outResponse, AIConcept_t con while( pCopy ) { - pCopy = SplitContext( pCopy, key, sizeof( key ), value, sizeof( value ), NULL ); + pCopy = SplitContext( pCopy, key, sizeof( key ), value, sizeof( value ), NULL, modifiers ); if( *key && *value ) { @@ -306,7 +483,7 @@ bool CAI_Expresser::SpeakFindResponse( AI_Response &outResponse, AIConcept_t con } } } -#endif + // Let our outer fill in most match criteria GetOuter()->ModifyOrAppendCriteria( set ); @@ -317,45 +494,154 @@ bool CAI_Expresser::SpeakFindResponse( AI_Response &outResponse, AIConcept_t con if( pPlayer ) pPlayer->ModifyOrAppendPlayerCriteria( set ); } +#else + AI_CriteriaSet localCriteriaSet; // put it on the stack so we don't deal with new/delete + if (criteria == NULL) + { + GatherCriteria( &localCriteriaSet, concept, NULL ); + criteria = &localCriteriaSet; + } +#endif + + /// intercept any deferred criteria that are being sent to world + AI_CriteriaSet worldWritebackCriteria; + AI_CriteriaSet::InterceptWorldSetContexts( criteria, &worldWritebackCriteria ); + + // Now that we have a criteria set, ask for a suitable response + bool found = rs->FindBestResponse( *criteria, outResponse, this ); + + if ( rr_debugresponses.GetInt() == 4 ) + { + if ( ( GetOuter()->MyNPCPointer() && GetOuter()->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT ) || GetOuter()->IsPlayer() ) + { + const char *pszName; + if ( GetOuter()->IsPlayer() ) + { + pszName = ((CBasePlayer*)GetOuter())->GetPlayerName(); + } + else + { + pszName = GetOuter()->GetDebugName(); + } + + if ( found ) + { + char response[ 256 ]; + outResponse.GetResponse( response, sizeof( response ) ); + + Warning( "RESPONSERULES: %s spoke '%s'. Found response '%s'.\n", pszName, (const char*)concept, response ); + } + else + { + Warning( "RESPONSERULES: %s spoke '%s'. Found no matching response.\n", pszName, (const char*)concept ); + } + } + } + + if ( !found ) + { + return false; + } + else if ( worldWritebackCriteria.GetCount() > 0 ) + { + Assert( CBaseEntity::Instance( INDEXENT( 0 ) )->IsWorld( ) ); + worldWritebackCriteria.WriteToEntity( CBaseEntity::Instance( INDEXENT( 0 ) ) ); + } + + if ( outResponse.IsEmpty() ) + { + // AssertMsg2( false, "RR: %s got empty but valid response for %s", GetOuter()->GetDebugName(), concept.GetStringConcept() ); + return false; + } + else + { + return true; + } +} + +#if 0 +//----------------------------------------------------------------------------- +// Purpose: Searches for a possible response; writes it into a response passed as +// parameter rather than new'ing one up. +// Input : concept - +// NULL - +// Output : bool : true on success, false on fail +//----------------------------------------------------------------------------- +AI_Response *CAI_Expresser::SpeakFindResponse( AI_Response *result, AIConcept_t &concept, AI_CriteriaSet *criteria ) +{ + Assert(response); + + IResponseSystem *rs = GetOuter()->GetResponseSystem(); + if ( !rs ) + { + Assert( !"No response system installed for CAI_Expresser::GetOuter()!!!" ); + return NULL; + } + +#if 0 + AI_CriteriaSet set; + // Always include the concept name + set.AppendCriteria( "concept", concept, CONCEPT_WEIGHT ); -#ifdef HL2_LAZUL // Always include any optional modifiers - if (modifiers) + if ( modifiers != NULL ) { - char copy_modifiers[255]; + char copy_modifiers[ 255 ]; const char *pCopy; - char key[128] = { 0 }; - char value[128] = { 0 }; + char key[ 128 ] = { 0 }; + char value[ 128 ] = { 0 }; - Q_strncpy(copy_modifiers, modifiers, sizeof(copy_modifiers)); + Q_strncpy( copy_modifiers, modifiers, sizeof( copy_modifiers ) ); pCopy = copy_modifiers; - while (pCopy) + while( pCopy ) { - pCopy = SplitContext(pCopy, key, sizeof(key), value, sizeof(value), NULL); + pCopy = SplitContext( pCopy, key, sizeof( key ), value, sizeof( value ), NULL, modifiers ); - if (*key && *value) + if( *key && *value ) { - set.AppendCriteria(key, value, CONCEPT_WEIGHT); + set.AppendCriteria( key, value, CONCEPT_WEIGHT ); } } } -#endif + + // Let our outer fill in most match criteria + GetOuter()->ModifyOrAppendCriteria( set ); + + // Append local player criteria to set, but not if this is a player doing the talking + if ( !GetOuter()->IsPlayer() ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + if( pPlayer ) + pPlayer->ModifyOrAppendPlayerCriteria( set ); + } +#else + AI_CriteriaSet &set = *criteria; +#endif // Now that we have a criteria set, ask for a suitable response - bool found = rs->FindBestResponse( set, outResponse, this ); + bool found = rs->FindBestResponse( set, *result, this ); - if ( rr_debugresponses.GetInt() == 3 ) + if ( rr_debugresponses.GetInt() == 4 ) { if ( ( GetOuter()->MyNPCPointer() && GetOuter()->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT ) || GetOuter()->IsPlayer() ) { - const char *pszName = GetOuter()->IsPlayer() ? - ((CBasePlayer*)GetOuter())->GetPlayerName() : GetOuter()->GetDebugName(); + const char *pszName; + if ( GetOuter()->IsPlayer() ) + { + pszName = ((CBasePlayer*)GetOuter())->GetPlayerName(); + } + else + { + pszName = GetOuter()->GetDebugName(); + } if ( found ) { - const char *szReponse = outResponse.GetResponsePtr(); - Warning( "RESPONSERULES: %s spoke '%s'. Found response '%s'.\n", pszName, concept, szReponse ); + char response[ 256 ]; + result->GetResponse( response, sizeof( response ) ); + + Warning( "RESPONSERULES: %s spoke '%s'. Found response '%s'.\n", pszName, concept, response ); } else { @@ -365,32 +651,81 @@ bool CAI_Expresser::SpeakFindResponse( AI_Response &outResponse, AIConcept_t con } if ( !found ) + { + //Assert( !"rs->FindBestResponse: Returned a NULL AI_Response!" ); return false; + } - const char *szReponse = outResponse.GetResponsePtr(); - if ( !szReponse[0] ) - return false; + char response[ 256 ]; + result->GetResponse( response, sizeof( response ) ); - if ( ( outResponse.GetOdds() < 100 ) && ( random->RandomInt( 1, 100 ) <= outResponse.GetOdds() ) ) + if ( !response[0] ) + { return false; + } return true; } +#endif //----------------------------------------------------------------------------- // Purpose: Dispatches the result // Input : *response - //----------------------------------------------------------------------------- -bool CAI_Expresser::SpeakDispatchResponse( AIConcept_t concept, AI_Response& response, IRecipientFilter *filter /* = NULL */ ) +bool CAI_Expresser::SpeakDispatchResponse( AIConcept_t &concept, AI_Response *result, AI_CriteriaSet *criteria, IRecipientFilter *filter /* = NULL */ ) { + char response[ 256 ]; + result->GetResponse( response, sizeof( response ) ); + + if (response[0] == '$') + { + const char* context = response + 1; + const char* replace = nullptr; + + int iSymbol = criteria->FindCriterionIndex(context); + if (criteria->IsValidIndex(iSymbol)) + { + replace = criteria->GetValue(iSymbol); + } + + if (replace) + { + DevMsg("Replacing %s with %s...\n", response, replace); + Q_strncpy(response, replace, sizeof(response)); + + // Precache it now because it may not have been precached before + switch (result->GetType()) + { + case RESPONSE_SPEAK: + { + GetOuter()->PrecacheScriptSound(response); + } + break; + + case RESPONSE_SCENE: + { + // TODO: Gender handling? + PrecacheInstancedScene(response); + } + break; + } + } + } + + float delay = result->GetDelay(); + bool spoke = false; - float delay = response.GetDelay(); - const char *szResponse = response.GetResponsePtr(); - soundlevel_t soundlevel = response.GetSoundLevel(); - if ( IsSpeaking() && concept[0] != 0 ) + soundlevel_t soundlevel = result->GetSoundLevel(); + + if ( IsSpeaking() && concept[0] != 0 && result->GetType() != ResponseRules::RESPONSE_PRINT ) { - DevMsg( "SpeakDispatchResponse: Entity ( %i/%s ) already speaking, forcing '%s'\n", GetOuter()->entindex(), STRING( GetOuter()->GetEntityName() ), concept ); + const char *entityName = STRING( GetOuter()->GetEntityName() ); + if ( GetOuter()->IsPlayer() ) + { + entityName = ToBasePlayer( GetOuter() )->GetPlayerName(); + } + DevMsg( 2, "SpeakDispatchResponse: Entity ( %i/%s ) already speaking, forcing '%s'\n", GetOuter()->entindex(), entityName ? entityName : "UNKNOWN", (const char*)concept ); // Tracker 15911: Can break the game if we stop an imported map placed lcs here, so only // cancel actor out of instanced scripted scenes. ywb @@ -399,53 +734,68 @@ bool CAI_Expresser::SpeakDispatchResponse( AIConcept_t concept, AI_Response& res if ( IsRunningScriptedScene( GetOuter() ) ) { - DevMsg( "SpeakDispatchResponse: Entity ( %i/%s ) refusing to speak due to scene entity, tossing '%s'\n", GetOuter()->entindex(), STRING( GetOuter()->GetEntityName() ), concept ); + DevMsg( "SpeakDispatchResponse: Entity ( %i/%s ) refusing to speak due to scene entity, tossing '%s'\n", GetOuter()->entindex(), entityName ? entityName : "UNKNOWN", (const char*)concept ); return false; } } - switch ( response.GetType() ) + switch ( result->GetType() ) { default: - case RESPONSE_NONE: + case ResponseRules::RESPONSE_NONE: break; - case RESPONSE_SPEAK: - if ( !response.ShouldntUseScene() ) - { - // This generates a fake CChoreoScene wrapping the sound.txt name - spoke = SpeakAutoGeneratedScene( szResponse, delay ); - } - else + case ResponseRules::RESPONSE_SPEAK: { - float speakTime = GetResponseDuration( response ); - GetOuter()->EmitSound( szResponse ); + if ( !result->ShouldntUseScene() ) + { + // This generates a fake CChoreoScene wrapping the sound.txt name + spoke = SpeakAutoGeneratedScene( response, delay ); + } + else + { + float speakTime = GetResponseDuration( result ); + GetOuter()->EmitSound( response ); - DevMsg( "SpeakDispatchResponse: Entity ( %i/%s ) playing sound '%s'\n", GetOuter()->entindex(), STRING( GetOuter()->GetEntityName() ), szResponse ); - NoteSpeaking( speakTime, delay ); - spoke = true; + DevMsg( 2, "SpeakDispatchResponse: Entity ( %i/%s ) playing sound '%s'\n", GetOuter()->entindex(), STRING( GetOuter()->GetEntityName() ), response ); + NoteSpeaking( speakTime, delay ); + spoke = true; + } } break; - case RESPONSE_SENTENCE: - spoke = ( -1 != SpeakRawSentence( szResponse, delay, VOL_NORM, soundlevel ) ) ? true : false; + case ResponseRules::RESPONSE_SENTENCE: + { + spoke = ( -1 != SpeakRawSentence( response, delay, VOL_NORM, soundlevel ) ) ? true : false; + } break; - case RESPONSE_SCENE: - spoke = SpeakRawScene( szResponse, delay, &response, filter ); + case ResponseRules::RESPONSE_SCENE: + { + spoke = SpeakRawScene( response, delay, result, filter ); + } break; - case RESPONSE_RESPONSE: - // This should have been recursively resolved already - Assert( 0 ); + case ResponseRules::RESPONSE_RESPONSE: + { + // This should have been recursively resolved already + Assert( 0 ); + } break; - case RESPONSE_PRINT: - if ( g_pDeveloper->GetInt() > 0 ) + case ResponseRules::RESPONSE_PRINT: { - Vector vPrintPos; - GetOuter()->CollisionProp()->NormalizedToWorldSpace( Vector(0.5,0.5,1.0f), &vPrintPos ); - NDebugOverlay::Text( vPrintPos, szResponse, true, 1.5 ); - spoke = true; + if ( g_pDeveloper->GetInt() > 0 ) + { + Vector vPrintPos; + GetOuter()->CollisionProp()->NormalizedToWorldSpace( Vector(0.5,0.5,1.0f), &vPrintPos ); + NDebugOverlay::Text( vPrintPos, response, true, 1.5 ); + } + spoke = true; + } + break; + case ResponseRules::RESPONSE_ENTITYIO: + { + return FireEntIOFromResponse( response, GetOuter() ); } break; } @@ -453,30 +803,107 @@ bool CAI_Expresser::SpeakDispatchResponse( AIConcept_t concept, AI_Response& res if ( spoke ) { m_flLastTimeAcceptedSpeak = gpGlobals->curtime; - if ( DebuggingSpeech() && g_pDeveloper->GetInt() > 0 && response.GetType() != RESPONSE_PRINT ) + if ( DebuggingSpeech() && g_pDeveloper->GetInt() > 0 && response && result->GetType() != ResponseRules::RESPONSE_PRINT ) { Vector vPrintPos; GetOuter()->CollisionProp()->NormalizedToWorldSpace( Vector(0.5,0.5,1.0f), &vPrintPos ); - NDebugOverlay::Text( vPrintPos, CFmtStr( "%s: %s", concept, szResponse ), true, 1.5 ); + NDebugOverlay::Text( vPrintPos, CFmtStr( "%s: %s", (const char*)concept, response ), true, 1.5 ); } - if ( response.IsApplyContextToWorld() ) + if (result->GetContext()) { - CBaseEntity *pEntity = CBaseEntity::Instance( engine->PEntityOfEntIndex( 0 ) ); - if ( pEntity ) + const char* pszContext = result->GetContext(); + + int iContextFlags = result->GetContextFlags(); + if (iContextFlags & APPLYCONTEXT_SQUAD) + { + CAI_BaseNPC* pNPC = GetOuter()->MyNPCPointer(); + if (pNPC && pNPC->GetSquad()) + { + AISquadIter_t iter; + CAI_BaseNPC* pSquadmate = pNPC->GetSquad()->GetFirstMember(&iter); + while (pSquadmate) + { + pSquadmate->AddContext(pszContext); + + pSquadmate = pNPC->GetSquad()->GetNextMember(&iter); + } + } + } + if (iContextFlags & APPLYCONTEXT_ENEMY) + { + CBaseEntity* pEnemy = GetOuter()->GetEnemy(); + if (pEnemy) + { + pEnemy->AddContext(pszContext); + } + } + if (iContextFlags & APPLYCONTEXT_WORLD) { - pEntity->AddContext( response.GetContext() ); + CBaseEntity* pEntity = CBaseEntity::Instance(engine->PEntityOfEntIndex(0)); + if (pEntity) + { + pEntity->AddContext(pszContext); + } + } + if (iContextFlags == 0 || iContextFlags & APPLYCONTEXT_SELF) + { + GetOuter()->AddContext(pszContext); } } - else + SetSpokeConcept( concept, result ); + } + else + { + } + + return spoke; +} + +bool CAI_Expresser::FireEntIOFromResponse( char *response, CBaseEntity *pInitiator ) +{ + // find the space-separator in the response name, then split into entityname, input, and parameter + // may barf in linux; there, should make some StringTokenizer() class that wraps the strtok_s behavior, etc. + char *pszEntname; + char *pszInput; + char *pszParam; + char *strtokContext; + + pszEntname = strtok_s( response, " ", &strtokContext ); + if ( !pszEntname ) + { + Warning( "Response was entityio but had bad value %s\n", response ); + return false; + } + + pszInput = strtok_s( NULL, " ", &strtokContext ); + if ( !pszInput ) + { + Warning( "Response was entityio but had bad value %s\n", response ); + return false; + } + + pszParam = strtok_s( NULL, " ", &strtokContext ); + + // poke entity io + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, pszEntname, pInitiator ); + if ( !pTarget ) + { + Msg( "Response rule targeted %s with entityio, but that doesn't exist.\n", pszEntname ); + // but this is actually a legit use case, so return true (below). + } + else + { + // pump the action into the target + variant_t variant; + if ( pszParam ) { - GetOuter()->AddContext( response.GetContext() ); + variant.SetString( MAKE_STRING(pszParam) ); } + pTarget->AcceptInput( pszInput, pInitiator, pInitiator, variant, 0 ); - SetSpokeConcept( concept, &response ); } - - return spoke; + return true; } //----------------------------------------------------------------------------- @@ -484,33 +911,45 @@ bool CAI_Expresser::SpeakDispatchResponse( AIConcept_t concept, AI_Response& res // Input : *response - // Output : float //----------------------------------------------------------------------------- -float CAI_Expresser::GetResponseDuration( AI_Response& response ) +float CAI_Expresser::GetResponseDuration( AI_Response *result ) { - const char *szResponse = response.GetResponsePtr(); + Assert( result ); + char response[ 256 ]; + result->GetResponse( response, sizeof( response ) ); - switch ( response.GetType() ) + switch ( result->GetType() ) { - default: - case RESPONSE_NONE: + case ResponseRules::RESPONSE_SPEAK: + { + return GetOuter()->GetSoundDuration( response, STRING( GetOuter()->GetModelName() ) ); + } break; - - case RESPONSE_SPEAK: - return GetOuter()->GetSoundDuration( szResponse, STRING( GetOuter()->GetModelName() ) ); - - case RESPONSE_SENTENCE: - Assert( 0 ); - return 999.0f; - - case RESPONSE_SCENE: - return GetSceneDuration( szResponse ); - - case RESPONSE_RESPONSE: - // This should have been recursively resolved already - Assert( 0 ); + case ResponseRules::RESPONSE_SENTENCE: + { + Assert( 0 ); + return 999.0f; + } break; - - case RESPONSE_PRINT: - return 1.0; + case ResponseRules::RESPONSE_SCENE: + { + return GetSceneDuration( response ); + } + break; + case ResponseRules::RESPONSE_RESPONSE: + { + // This should have been recursively resolved already + Assert( 0 ); + } + break; + case ResponseRules::RESPONSE_PRINT: + { + return 1.0; + } + break; + default: + case ResponseRules::RESPONSE_NONE: + case ResponseRules::RESPONSE_ENTITYIO: + return 0.0f; } return 0.0f; @@ -521,22 +960,44 @@ float CAI_Expresser::GetResponseDuration( AI_Response& response ) // Input : concept - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- -bool CAI_Expresser::Speak( AIConcept_t concept, const char *modifiers /*= NULL*/, char *pszOutResponseChosen /* = NULL*/, size_t bufsize /* = 0 */, IRecipientFilter *filter /* = NULL */ ) +bool CAI_Expresser::Speak( AIConcept_t &concept, const char *modifiers /*= NULL*/, char *pszOutResponseChosen /* = NULL*/, size_t bufsize /* = 0 */, IRecipientFilter *filter /* = NULL */ ) +{ + concept.SetSpeaker(GetOuter()); + AI_CriteriaSet criteria; + GatherCriteria(&criteria, concept, modifiers); + + return Speak( concept, &criteria, pszOutResponseChosen, bufsize, filter ); +} + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CAI_Expresser::Speak( AIConcept_t &concept, AI_CriteriaSet * RESTRICT criteria, char *pszOutResponseChosen , size_t bufsize , IRecipientFilter *filter ) { - AI_Response response; - bool result = SpeakFindResponse( response, concept, modifiers ); - if ( !result ) + VPROF("CAI_Expresser::Speak"); + if ( IsSpeechGloballySuppressed() ) + { return false; + } - SpeechMsg( GetOuter(), "%s (%p) spoke %s (%f)\n", STRING(GetOuter()->GetEntityName()), GetOuter(), concept, gpGlobals->curtime ); + GetOuter()->ModifyOrAppendDerivedCriteria(*criteria); + AI_Response result; + if ( !FindResponse( result, concept, criteria ) ) + { + return false; + } - bool spoke = SpeakDispatchResponse( concept, response, filter ); + SpeechMsg( GetOuter(), "%s (%x) spoke %s (%f)", STRING(GetOuter()->GetEntityName()), GetOuter(), (const char*)concept, gpGlobals->curtime ); + // Msg( "%s:%s to %s:%s\n", GetOuter()->GetDebugName(), concept.GetStringConcept(), criteria.GetValue(criteria.FindCriterionIndex("Subject")), pTarget ? pTarget->GetDebugName() : "none" ); + + bool spoke = SpeakDispatchResponse( concept, &result, criteria, filter ); if ( pszOutResponseChosen ) { - const char *szResponse = response.GetResponsePtr(); - Q_strncpy( pszOutResponseChosen, szResponse, bufsize ); + result.GetResponse( pszOutResponseChosen, bufsize ); } - + return spoke; } @@ -550,7 +1011,7 @@ bool CAI_Expresser::SpeakRawScene( const char *pszScene, float delay, AI_Respons { SpeechMsg( GetOuter(), "SpeakRawScene( %s, %f) %f\n", pszScene, delay, sceneLength ); -#if defined( HL2_EPISODIC ) || defined( TF_DLL ) || defined( TF_CLASSIC ) +#if defined( HL2_EPISODIC ) char szInstanceFilename[256]; GetOuter()->GenderExpandString( pszScene, szInstanceFilename, sizeof( szInstanceFilename ) ); // Only mark ourselves as speaking if the scene has speech @@ -604,9 +1065,6 @@ int CAI_Expresser::SpeakRawSentence( const char *pszSentence, float delay, float } else { - if (pszSentence[0] == '~') - pszSentence++; - sentenceIndex = SENTENCEG_PlayRndSz( GetOuter()->NetworkProp()->edict(), pszSentence, volume, soundlevel, 0, GetVoicePitch() ); } @@ -730,14 +1188,14 @@ bool CAI_Expresser::CanSpeakConcept( AIConcept_t concept ) ConceptHistory_t *history = &m_ConceptHistories[iter]; Assert( history ); - AI_Response *response = history->response; - if ( !response ) + const AI_Response &response = history->m_response; + if ( response.IsEmpty() ) return true; - if ( response->GetSpeakOnce() ) + if ( response.GetSpeakOnce() ) return false; - float respeakDelay = response->GetRespeakDelay(); + float respeakDelay = response.GetRespeakDelay(); if ( respeakDelay != 0.0f ) { @@ -782,12 +1240,10 @@ void CAI_Expresser::SetSpokeConcept( AIConcept_t concept, AI_Response *response, ConceptHistory_t *slot = &m_ConceptHistories[ idx ]; slot->timeSpoken = gpGlobals->curtime; - // Update response info if ( response ) { - delete slot->response; - slot->response = new AI_Response( *response ); + slot->m_response = *response; } if ( bCallback ) @@ -818,7 +1274,7 @@ void CAI_Expresser::DumpHistories() bool CAI_Expresser::IsValidResponse( ResponseType_t type, const char *pszValue ) { - if ( type == RESPONSE_SCENE ) + if ( type == ResponseRules::RESPONSE_SCENE ) { char szInstanceFilename[256]; GetOuter()->GenderExpandString( pszValue, szInstanceFilename, sizeof( szInstanceFilename ) ); @@ -833,7 +1289,7 @@ bool CAI_Expresser::IsValidResponse( ResponseType_t type, const char *pszValue ) CAI_TimedSemaphore *CAI_Expresser::GetMySpeechSemaphore( CBaseEntity *pNpc ) { if ( !pNpc->MyNPCPointer() ) - return NULL; + return false; return (pNpc->MyNPCPointer()->IsPlayerAlly() ? &g_AIFriendliesTalkSemaphore : &g_AIFoesTalkSemaphore ); } @@ -846,23 +1302,26 @@ void CAI_Expresser::SpeechMsg( CBaseEntity *pFlex, const char *pszFormat, ... ) if ( !DebuggingSpeech() ) return; - char string[ 2048 ]; - va_list argptr; - va_start( argptr, pszFormat ); - Q_vsnprintf( string, sizeof(string), pszFormat, argptr ); - va_end( argptr ); - if ( pFlex->MyNPCPointer() ) { - DevMsg( pFlex->MyNPCPointer(), "%s", string ); + + DevMsg( pFlex->MyNPCPointer(), CFmtStr( &pszFormat ) ); } else { - DevMsg( "%s", string ); + DevMsg( CFmtStr( &pszFormat ) ); } - UTIL_LogPrintf( "%s", string ); + UTIL_LogPrintf( (char *) ( (const char *) CFmtStr( &pszFormat ) ) ); } +//----------------------------------------------------------------------------- +// Purpose: returns true when l4d is in credits screen or some other +// speech-forbidden state +//----------------------------------------------------------------------------- +bool CAI_Expresser::IsSpeechGloballySuppressed() +{ + return false; +} //----------------------------------------------------------------------------- @@ -883,16 +1342,7 @@ void CAI_ExpresserHost_NPC_DoModifyOrAppendCriteria( CAI_BaseNPC *pSpeaker, AI_C if ( pSpeaker->GetEnemy() ) { - CBaseEntity* pEnemy = pSpeaker->GetEnemy(); - set.AppendCriteria( "enemy", pEnemy->GetClassname() ); - set.AppendCriteria("enemyclass", g_pGameRules->AIClassText(pEnemy->Classify())); - float healthfrac = 0.0f; - if (pEnemy->GetMaxHealth() > 0) - { - healthfrac = (float)pEnemy->GetHealth() / (float)pEnemy->GetMaxHealth(); - } - - set.AppendCriteria("enemyhealthfrac", UTIL_VarArgs("%.3f", healthfrac)); + set.AppendCriteria( "enemy", pSpeaker->GetEnemy()->GetClassname() ); set.AppendCriteria( "timesincecombat", "-1" ); } else @@ -915,7 +1365,7 @@ void CAI_ExpresserHost_NPC_DoModifyOrAppendCriteria( CAI_BaseNPC *pSpeaker, AI_C set.AppendCriteria( "weapon", "none" ); } - CBasePlayer *pPlayer = pSpeaker->GetBestPlayer(); + CBasePlayer *pPlayer = AI_GetSinglePlayer(); if ( pPlayer ) { Vector distance = pPlayer->GetAbsOrigin() - pSpeaker->GetAbsOrigin(); @@ -948,18 +1398,9 @@ void CAI_ExpresserHost_NPC_DoModifyOrAppendCriteria( CAI_BaseNPC *pSpeaker, AI_C } //----------------------------------------------------------------------------- - -//============================================================================= -// HPE_BEGIN: -// [Forrest] Remove npc_speakall from Counter-Strike. -//============================================================================= -#ifndef CSTRIKE_DLL -extern CBaseEntity *FindPickerEntity( CBasePlayer *pPlayer ); +extern CBaseEntity* FindPickerEntity(CBasePlayer* pPlayer); CON_COMMAND( npc_speakall, "Force the npc to try and speak all their responses" ) { - if ( !UTIL_IsCommandIssuedByServerAdmin() ) - return; - CBaseEntity *pEntity; if ( args[1] && *args[1] ) @@ -972,9 +1413,9 @@ CON_COMMAND( npc_speakall, "Force the npc to try and speak all their responses" } else { - pEntity = FindPickerEntity( UTIL_GetCommandClient() ); + pEntity = UTIL_GetCommandClient() ? FindPickerEntity(UTIL_GetCommandClient()) : NULL; } - + if ( pEntity ) { CAI_BaseNPC *pNPC = pEntity->MyNPCPointer(); @@ -989,14 +1430,9 @@ CON_COMMAND( npc_speakall, "Force the npc to try and speak all their responses" } } } -#endif -//============================================================================= -// HPE_END -//============================================================================= - //----------------------------------------------------------------------------- -CMultiplayer_Expresser::CMultiplayer_Expresser( CBaseFlex *pOuter ) : CAI_Expresser( pOuter ) +CMultiplayer_Expresser::CMultiplayer_Expresser( CBaseFlex *pOuter ) : CAI_ExpresserWithFollowup( pOuter ) { m_bAllowMultipleScenes = false; } @@ -1020,4 +1456,4 @@ void CMultiplayer_Expresser::AllowMultipleScenes() void CMultiplayer_Expresser::DisallowMultipleScenes() { m_bAllowMultipleScenes = false; -} \ No newline at end of file +} diff --git a/src/game/server/ai_speech.h b/src/game/server/ai_speech.h index fa173c1ed..12e51e0f8 100644 --- a/src/game/server/ai_speech.h +++ b/src/game/server/ai_speech.h @@ -1,4 +1,4 @@ -//========= Copyright Valve Corporation, All rights reserved. ============// +//========= Copyright (c) 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // @@ -11,15 +11,20 @@ #include "utlmap.h" #include "soundflags.h" -#include "AI_ResponseSystem.h" +#include "AI_Criteria.h" +#include "ai_responsesystem.h" #include "utldict.h" +#include "ai_speechconcept.h" #if defined( _WIN32 ) #pragma once #endif class KeyValues; -class AI_CriteriaSet; + +using ResponseRules::ResponseType_t; +using ResponseRules::AI_ResponseFollowup; + //----------------------------------------------------------------------------- // Purpose: Used to share a global resource or prevent a system stepping on @@ -63,8 +68,6 @@ extern CAI_TimedSemaphore g_AIFoesTalkSemaphore; // Constants -const float AIS_DEF_MIN_DELAY = 2.8; // Minimum amount of time an NPCs will wait after someone has spoken before considering speaking again -const float AIS_DEF_MAX_DELAY = 3.2; // Maximum amount of time an NPCs will wait after someone has spoken before considering speaking again const float AIS_NO_DELAY = 0; const soundlevel_t AIS_DEF_SNDLVL = SNDLVL_TALKING; #define AI_NULL_CONCEPT NULL @@ -92,17 +95,20 @@ const soundlevel_t AIS_DEF_SNDLVL = SNDLVL_TALKING; // An id that represents the core meaning of a spoken phrase, // eventually to be mapped to a sentence group or scene +#if AI_CONCEPTS_ARE_STRINGS typedef const char *AIConcept_t; - inline bool CompareConcepts( AIConcept_t c1, AIConcept_t c2 ) { return ( (void *)c1 == (void *)c2 || ( c1 && c2 && Q_stricmp( c1, c2 ) == 0 ) ); } +#else +typedef CAI_Concept AIConcept_t; +inline bool CompareConcepts( AIConcept_t c1, AIConcept_t c2 ) +{ + return c1.m_iConcept == c2.m_iConcept; +} +#endif -//------------------------------------- -// Specifies and stores the base timing and attentuation values for concepts -// -class AI_Response; //----------------------------------------------------------------------------- // CAI_Expresser @@ -127,7 +133,7 @@ struct ConceptHistory_t DECLARE_SIMPLE_DATADESC(); ConceptHistory_t(float timeSpoken = -1 ) - : timeSpoken( timeSpoken ), response( NULL ) + : timeSpoken( timeSpoken ), m_response( ) { } @@ -135,13 +141,13 @@ struct ConceptHistory_t ConceptHistory_t& operator = ( const ConceptHistory_t& src ); ~ConceptHistory_t(); - + float timeSpoken; - AI_Response *response; + AI_Response m_response; }; //------------------------------------- -class CAI_Expresser : public IResponseFilter +class CAI_Expresser : public ResponseRules::IResponseFilter { public: CAI_Expresser( CBaseFlex *pOuter = NULL ); @@ -156,18 +162,32 @@ class CAI_Expresser : public IResponseFilter // -------------------------------- - bool Speak( AIConcept_t concept, const char *modifiers = NULL, char *pszOutResponseChosen = NULL, size_t bufsize = 0, IRecipientFilter *filter = NULL ); + bool Speak( AIConcept_t &concept, const char *modifiers = NULL, char *pszOutResponseChosen = NULL, size_t bufsize = 0, IRecipientFilter *filter = NULL ); + bool Speak( AIConcept_t &concept, AI_CriteriaSet *criteria, char *pszOutResponseChosen = NULL, size_t bufsize = 0, IRecipientFilter *filter = NULL ); + // Given modifiers (which are colon-delimited strings), fill out a criteria set including this + // character's contexts and the ones in the modifier. This lets us hang on to them after a call + // to SpeakFindResponse. + void GatherCriteria( AI_CriteriaSet *outputCritera, const AIConcept_t &concept, const char *modifiers ); // These two methods allow looking up a response and dispatching it to be two different steps - bool SpeakFindResponse( AI_Response &response, AIConcept_t concept, const char *modifiers = NULL ); - bool SpeakDispatchResponse( AIConcept_t concept, AI_Response &response, IRecipientFilter *filter = NULL ); - float GetResponseDuration( AI_Response &response ); + // AI_Response *SpeakFindResponse( AIConcept_t concept, const char *modifiers = NULL ); + // AI_Response *SpeakFindResponse( AIConcept_t &concept, AI_CriteriaSet *criteria ); + // Find the appropriate response for the given concept. Return false if none found. + // Fills out the response object that you provide. + bool FindResponse( AI_Response &outResponse, AIConcept_t &concept, AI_CriteriaSet *modifiers = NULL ); + virtual bool SpeakDispatchResponse( AIConcept_t &concept, AI_Response *response, AI_CriteriaSet *criteria, IRecipientFilter *filter = NULL ); + float GetResponseDuration( AI_Response *response ); virtual int SpeakRawSentence( const char *pszSentence, float delay, float volume = VOL_NORM, soundlevel_t soundlevel = SNDLVL_TALKING, CBaseEntity *pListener = NULL ); bool SemaphoreIsAvailable( CBaseEntity *pTalker ); float GetSemaphoreAvailableTime( CBaseEntity *pTalker ); + virtual void OnSpeechFinished() {}; + + // This function can be overriden by games to suppress speech altogether during glue screens, etc + static bool IsSpeechGloballySuppressed(); + // -------------------------------- virtual bool IsSpeaking(); @@ -194,6 +214,12 @@ class CAI_Expresser : public IResponseFilter // Force the NPC to release the semaphore & clear next speech time void ForceNotSpeaking( void ); + // helper used in dealing with RESPONSE_ENTITYIO + // response is the output of AI_Response::GetName + // note: the response string will get stomped on (by strtok) + // returns false on failure (eg, couldn't match parse contents) + static bool FireEntIOFromResponse( char *response, CBaseEntity *pInitiator ); + protected: CAI_TimedSemaphore *GetMySpeechSemaphore( CBaseEntity *pNpc ); @@ -203,7 +229,7 @@ class CAI_Expresser : public IResponseFilter void DumpHistories(); - void SpeechMsg( CBaseEntity *pFlex, PRINTF_FORMAT_STRING const char *pszFormat, ... ); + void SpeechMsg( CBaseEntity *pFlex, const char *pszFormat, ... ); // -------------------------------- @@ -241,7 +267,7 @@ class CAI_Expresser : public IResponseFilter // -------------------------------- // public: - virtual void SetOuter( CBaseFlex *pOuter ) { m_pOuter = pOuter; } + void SetOuter( CBaseFlex *pOuter ); CBaseFlex * GetOuter() { return m_pOuter; } const CBaseFlex * GetOuter() const { return m_pOuter; } @@ -250,22 +276,6 @@ class CAI_Expresser : public IResponseFilter CHandle m_pOuter; }; -class CMultiplayer_Expresser : public CAI_Expresser -{ -public: - CMultiplayer_Expresser( CBaseFlex *pOuter = NULL ); - //~CMultiplayer_Expresser(); - - virtual bool IsSpeaking(); - - void AllowMultipleScenes(); - void DisallowMultipleScenes(); - -private: - bool m_bAllowMultipleScenes; - -}; - //----------------------------------------------------------------------------- // // An NPC base class to assist a branch of the inheritance graph @@ -281,12 +291,21 @@ class CAI_ExpresserHost : public BASE_NPC, protected CAI_ExpresserSink 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 ); + virtual bool Speak( AIConcept_t concept, AI_CriteriaSet *pCriteria, char *pszOutResponseChosen = NULL, size_t bufsize = 0, IRecipientFilter *filter = NULL ); + + void GatherCriteria( AI_CriteriaSet *outputCritera, const AIConcept_t &concept, const char *modifiers ); // These two methods allow looking up a response and dispatching it to be two different steps - bool SpeakFindResponse( AI_Response& response, AIConcept_t concept, const char *modifiers = NULL ); - bool SpeakDispatchResponse( AIConcept_t concept, AI_Response& response ); - virtual void PostSpeakDispatchResponse( AIConcept_t concept, AI_Response& response ) { return; } - float GetResponseDuration( AI_Response& response ); + bool SpeakFindResponse(AI_Response& outResponse, AIConcept_t concept, const char *modifiers = NULL ); + // AI_Response *SpeakFindResponse( AIConcept_t concept, AI_CriteriaSet *criteria ); + // AI_Response *SpeakFindResponse( AIConcept_t concept ); + // Find the appropriate response for the given concept. Return false if none found. + // Fills out the response object that you provide. + bool FindResponse( AI_Response &outResponse, AIConcept_t &concept, AI_CriteriaSet *criteria = NULL ); + + bool SpeakDispatchResponse( AIConcept_t concept, AI_Response *response, AI_CriteriaSet *criteria = NULL ); + virtual void PostSpeakDispatchResponse( AIConcept_t concept, AI_Response *response ) { return; } + float GetResponseDuration( AI_Response *response ); float GetTimeSpeechComplete() const { return this->GetExpresser()->GetTimeSpeechComplete(); } @@ -302,7 +321,7 @@ class CAI_ExpresserHost : public BASE_NPC, protected CAI_ExpresserSink int PlaySentence( const char *pszSentence, float delay, float volume = VOL_NORM, soundlevel_t soundlevel = SNDLVL_TALKING, CBaseEntity *pListener = NULL ); virtual void ModifyOrAppendCriteria( AI_CriteriaSet& set ); - virtual IResponseSystem *GetResponseSystem(); + virtual ResponseRules::IResponseSystem *GetResponseSystem(); // Override of base entity response input handler virtual void DispatchResponse( const char *conceptName ); }; @@ -324,6 +343,21 @@ inline bool CAI_ExpresserHost::Speak( AIConcept_t concept, const char return this->GetExpresser()->Speak( concept, modifiers, pszOutResponseChosen, bufsize, filter ); } +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +template +inline bool CAI_ExpresserHost::Speak( AIConcept_t concept, AI_CriteriaSet *pCriteria, char *pszOutResponseChosen /*=NULL*/, size_t bufsize /* = 0 */, IRecipientFilter *filter /* = NULL */ ) +{ + AssertOnce( this->GetExpresser()->GetOuter() == this ); + CAI_Expresser * const RESTRICT pExpresser = this->GetExpresser(); + concept.SetSpeaker(this); + // add in any local criteria to the one passed on the command line. + pExpresser->GatherCriteria( pCriteria, concept, NULL ); + // call the "I have aleady gathered criteria" version of Expresser::Speak + return pExpresser->Speak( concept, pCriteria, pszOutResponseChosen, bufsize, filter ); +} + + //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- template @@ -341,36 +375,86 @@ inline void CAI_ExpresserHost::ModifyOrAppendCriteria( AI_CriteriaSet& { BaseClass::ModifyOrAppendCriteria( criteriaSet ); + if ( this->MyNPCPointer() ) { CAI_ExpresserHost_NPC_DoModifyOrAppendCriteria( this->MyNPCPointer(), criteriaSet ); } + } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- template -inline IResponseSystem *CAI_ExpresserHost::GetResponseSystem() +inline ResponseRules::IResponseSystem *CAI_ExpresserHost::GetResponseSystem() { - extern IResponseSystem *g_pResponseSystem; + extern ResponseRules::IResponseSystem *g_pResponseSystem; // Expressive NPC's use the general response system return g_pResponseSystem; } + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +template +inline void CAI_ExpresserHost::GatherCriteria( AI_CriteriaSet *outputCriteria, const AIConcept_t &concept, const char *modifiers ) +{ + return this->GetExpresser()->GatherCriteria( outputCriteria, concept, modifiers ); +} + + +#if 1 +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +template +inline bool CAI_ExpresserHost::SpeakFindResponse(AI_Response& outResponse, AIConcept_t concept, const char *modifiers /*= NULL*/ ) +{ + AI_CriteriaSet criteria; + GatherCriteria(&criteria, concept, modifiers); + return FindResponse(outResponse, concept, &criteria); +} +#endif + +#if 0 +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +template +inline AI_Response *CAI_ExpresserHost::SpeakFindResponse( AIConcept_t concept, AI_CriteriaSet *criteria /*= NULL*/ ) +{ + return this->GetExpresser()->SpeakFindResponse( concept, criteria ); +} + + +//----------------------------------------------------------------------------- +// In this case we clearly don't care to hang on to the criteria, so make a convenience +// class that generates a one off. +//----------------------------------------------------------------------------- +template +inline AI_Response * CAI_ExpresserHost::SpeakFindResponse( AIConcept_t concept ) +{ + AI_CriteriaSet criteria; + GatherCriteria( &criteria, concept, NULL ); + return this->GetExpresser()->SpeakFindResponse( concept, &criteria ); +} +#endif + + //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- template -inline bool CAI_ExpresserHost::SpeakFindResponse( AI_Response& response, AIConcept_t concept, const char *modifiers /*= NULL*/ ) +inline bool CAI_ExpresserHost::FindResponse( AI_Response &outResponse, AIConcept_t &concept, AI_CriteriaSet *criteria ) { - return this->GetExpresser()->SpeakFindResponse( response, concept, modifiers ); + return this->GetExpresser()->FindResponse( outResponse, concept, criteria ); } + //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- template -inline bool CAI_ExpresserHost::SpeakDispatchResponse( AIConcept_t concept, AI_Response& response ) +inline bool CAI_ExpresserHost::SpeakDispatchResponse( AIConcept_t concept, AI_Response *response, AI_CriteriaSet *criteria ) { - if ( this->GetExpresser()->SpeakDispatchResponse( concept, response ) ) + if ( this->GetExpresser()->SpeakDispatchResponse( concept, response, criteria ) ) { PostSpeakDispatchResponse( concept, response ); return true; @@ -382,7 +466,7 @@ inline bool CAI_ExpresserHost::SpeakDispatchResponse( AIConcept_t conc //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- template -inline float CAI_ExpresserHost::GetResponseDuration( AI_Response& response ) +inline float CAI_ExpresserHost::GetResponseDuration( AI_Response *response ) { return this->GetExpresser()->GetResponseDuration( response ); } @@ -398,4 +482,160 @@ inline void CAI_ExpresserHost::DispatchResponse( const char *conceptNa //----------------------------------------------------------------------------- +/// A shim under CAI_ExpresserHost you can use when deriving a new expresser +/// host type under CAI_BaseNPC. This does the extra step of declaring an m_pExpresser +/// member and initializing it from CreateComponents(). If your BASE_NPC class isn't +/// actually an NPC, then CreateComponents() never gets called and you won't have +/// an expresser created. +/// Note: you still need to add m_pExpresser to the Datadesc for your derived type. +/// This is because I couldn't figure out how to make a templatized datadesc declaration +/// that works generically on the template type. +template +class CAI_ExpresserHostWithData : public CAI_ExpresserHost +{ + DECLARE_CLASS_NOFRIEND( CAI_ExpresserHostWithData, CAI_ExpresserHost ); + +public: + CAI_ExpresserHostWithData( ) : m_pExpresser(NULL) {}; + + virtual CAI_Expresser *GetExpresser() { return m_pExpresser; } + const CAI_Expresser *GetExpresser() const { return m_pExpresser; } + + virtual bool CreateComponents() + { + return BaseClass::CreateComponents() && ( CreateExpresser() != NULL ); + } + +protected: + EXPRESSER_TYPE *CreateExpresser( void ) + { + AssertMsg1( m_pExpresser == NULL, "Tried to double-initialize expresser in %s\n", GetDebugName() ); + m_pExpresser = new EXPRESSER_TYPE(this); + if ( !m_pExpresser) + { + AssertMsg1( false, "Creating an expresser failed in %s\n", GetDebugName() ); + return NULL; + } + + m_pExpresser->Connect(this); + return m_pExpresser; + } + + virtual ~CAI_ExpresserHostWithData( void ) + { + delete m_pExpresser; + m_pExpresser = NULL; + } + + EXPRESSER_TYPE *m_pExpresser; +}; + +/// response rules +namespace RR +{ + /// some applycontext clauses have operators preceding them, + /// like ++1 which means "take the current value and increment it + /// by one". These classes detect these cases and do the appropriate + /// thing. + class CApplyContextOperator + { + public: + inline CApplyContextOperator( int nSkipChars ) : m_nSkipChars(nSkipChars) {}; + + /// perform whatever this operator does upon the given context value. + /// Default op is simply to copy old to new. + /// pOldValue should be the currently set value of the context. May be NULL meaning no prior value. + /// pOperator the value that applycontext says to set + /// pNewValue a pointer to a buffer where the real new value will be writ. + /// returns true on success; false on failure (eg, tried to increment a + /// non-numeric value). + virtual bool Apply( const char *pOldValue, const char *pOperator, char *pNewValue, int pNewValBufSize ); + + /// This is the function that should be called from outside, + /// fed the input string, it'll select the right operator + /// to apply. + static CApplyContextOperator *FindOperator( const char *pContextString ); + + protected: + int m_nSkipChars; // how many chars to "skip" in the value string to get past the op specifier to the actual value + // eg, "++3" has a m_nSkipChars of 2, because the op string "++" is two characters. + }; + + class CIncrementOperator : public CApplyContextOperator + { + public: + inline CIncrementOperator( int nSkipChars ) : CApplyContextOperator(nSkipChars) {}; + virtual bool Apply( const char *pOldValue, const char *pOperator, char *pNewValue, int pNewValBufSize ); + }; + + class CDecrementOperator : public CApplyContextOperator + { + public: + inline CDecrementOperator( int nSkipChars ) : CApplyContextOperator(nSkipChars) {}; + virtual bool Apply( const char *pOldValue, const char *pOperator, char *pNewValue, int pNewValBufSize ); + }; + + class CToggleOperator : public CApplyContextOperator + { + public: + inline CToggleOperator( int nSkipChars ) : CApplyContextOperator(nSkipChars) {}; + virtual bool Apply( const char *pOldValue, const char *pOperator, char *pNewValue, int pNewValBufSize ); + }; + + // the singleton operators + extern CApplyContextOperator sm_OpCopy; + extern CIncrementOperator sm_OpIncrement; + extern CDecrementOperator sm_OpDecrement; + extern CToggleOperator sm_OpToggle; +}; + + +//----------------------------------------------------------------------------- +#include "ai_speechqueue.h" + +//----------------------------------------------------------------------------- +// A kind of AI Expresser that can dispatch a follow-up speech event when it +// finishes speaking. +//----------------------------------------------------------------------------- +class CAI_ExpresserWithFollowup : public CAI_Expresser +{ +public: + CAI_ExpresserWithFollowup( CBaseFlex *pOuter = NULL ) : CAI_Expresser(pOuter), + m_pPostponedFollowup(NULL) + {}; + virtual bool Speak( AIConcept_t &concept, const char *modifiers = NULL, char *pszOutResponseChosen = NULL, size_t bufsize = 0, IRecipientFilter *filter = NULL ); + virtual bool SpeakDispatchResponse( AIConcept_t &concept, AI_Response *response, AI_CriteriaSet *criteria, IRecipientFilter *filter = NULL ); + virtual void SpeakDispatchFollowup( AI_ResponseFollowup &followup ); + + virtual void OnSpeechFinished(); + + typedef CAI_Expresser BaseClass; +protected: + static void DispatchFollowupThroughQueue( const AIConcept_t &concept, + const char *criteriaStr, + const CResponseQueue::CFollowupTargetSpec_t &target, + float delay, + CBaseEntity * RESTRICT pOuter ); + + AI_ResponseFollowup *m_pPostponedFollowup; // TODO: save/restore + CResponseQueue::CFollowupTargetSpec_t m_followupTarget; +}; + +class CMultiplayer_Expresser : public CAI_ExpresserWithFollowup +{ +public: + CMultiplayer_Expresser( CBaseFlex *pOuter = NULL ); + //~CMultiplayer_Expresser(); + + virtual bool IsSpeaking(); + + void AllowMultipleScenes(); + void DisallowMultipleScenes(); + +private: + bool m_bAllowMultipleScenes; + +}; + + #endif // AI_SPEECH_H diff --git a/src/game/server/ai_speechconcept.cpp b/src/game/server/ai_speechconcept.cpp new file mode 100644 index 000000000..c0ae8e36b --- /dev/null +++ b/src/game/server/ai_speechconcept.cpp @@ -0,0 +1,28 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" + +#include "ai_speechconcept.h" + +#ifdef GAME_DLL +#include "game.h" +#include "ai_basenpc.h" +#include "sceneentity.h" +#endif + +#include "engine/ienginesound.h" +#include "keyvalues.h" +#include "ai_criteria.h" +#include "isaverestore.h" + + +// memdbgon must be the last include file in a .cpp file!!! +#include + + +// empty \ No newline at end of file diff --git a/src/game/server/ai_speechconcept.h b/src/game/server/ai_speechconcept.h new file mode 100644 index 000000000..41a3cc60b --- /dev/null +++ b/src/game/server/ai_speechconcept.h @@ -0,0 +1,45 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: Class data for an AI Concept, an atom of response-driven dialog. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef AI_SPEECHCONCEPT_H +#define AI_SPEECHCONCEPT_H + +#if defined( _WIN32 ) +#pragma once +#endif + +#include "../../public/responserules/response_types.h" + +class CAI_Concept : public ResponseRules::CRR_Concept +{ +public: + CAI_Concept() {}; + // construct concept from a string. + CAI_Concept(const char *fromString) : CRR_Concept(fromString) {} ; + + // get/set BS + inline EHANDLE GetSpeaker() const { return m_hSpeaker; } + inline void SetSpeaker(EHANDLE val) { m_hSpeaker = val; } + + /* + inline EHANDLE GetTarget() const { return m_hTarget; } + inline void SetTarget(EHANDLE val) { m_hTarget = val; } + inline EHANDLE GetTopic() const { return m_hTopic; } + inline void SetTopic(EHANDLE val) { m_hTopic = val; } + */ + +protected: + EHANDLE m_hSpeaker; + + /* + EHANDLE m_hTarget; + EHANDLE m_hTopic; + */ +}; + + +#endif diff --git a/src/game/server/ai_speechqueue.cpp b/src/game/server/ai_speechqueue.cpp new file mode 100644 index 000000000..5f9a2e0df --- /dev/null +++ b/src/game/server/ai_speechqueue.cpp @@ -0,0 +1,479 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" + +#include "basemultiplayerplayer.h" +#include "ai_baseactor.h" +#include "ai_speech.h" +//#include "flex_expresser.h" +// memdbgon must be the last include file in a .cpp file!!! +#include + +extern ConVar ai_debug_speech; +#define DebuggingSpeech() ai_debug_speech.GetBool() +extern ConVar rr_debugresponses; + +ConVar rr_followup_maxdist( "rr_followup_maxdist", "1800", FCVAR_CHEAT, "'then ANY' or 'then ALL' response followups will be dispatched only to characters within this distance." ); + +/////////////////////////////////////////////////////////////////////////////// +// RESPONSE QUEUE DATA STRUCTURE +/////////////////////////////////////////////////////////////////////////////// + +CResponseQueue::CResponseQueue( int queueSize ) : m_Queue(queueSize), m_ExpresserTargets(8,8) +{}; + +/// Add a deferred response. +void CResponseQueue::Add( const AIConcept_t &concept, ///< concept to dispatch + const AI_CriteriaSet * RESTRICT contexts, + float time, ///< when to dispatch it. You can specify a time of zero to mean "immediately." + const CFollowupTargetSpec_t &targetspec, + CBaseEntity *pIssuer + ) +{ + // Add a response. + AssertMsg( m_Queue.Count() < AI_RESPONSE_QUEUE_SIZE, "AI Response queue overfilled." ); + QueueType_t::IndexLocalType_t idx = m_Queue.AddToTail(); + m_Queue[idx].Init( concept, contexts, time, targetspec, pIssuer ); +} + + +/// Remove a deferred response matching the concept and issuer. +void CResponseQueue::Remove( const AIConcept_t &concept, ///< concept to dispatch + CBaseEntity * const RESTRICT pIssuer ///< the entity issuing the response, if one exists. + ) RESTRICT +{ + // walk through the queue until we find a response matching the concept and issuer, then strike it. + QueueType_t::IndexLocalType_t idx = m_Queue.Head(); + while (idx != m_Queue.InvalidIndex()) + { + CDeferredResponse &response = m_Queue[idx]; + QueueType_t::IndexLocalType_t previdx = idx; // advance the index immediately because we may be deleting the "current" element + idx = m_Queue.Next(idx); // is now the next index + if ( CompareConcepts( response.m_concept, concept ) && // if concepts match and + ( !pIssuer || ( response.m_hIssuer.Get() == pIssuer ) ) // issuer is null, or matches the one in the response + ) + { + m_Queue.Remove(previdx); + } + } +} + + +void CResponseQueue::RemoveSpeechQueuedFor( const CBaseEntity *pSpeaker ) +{ + // walk through the queue until we find a response matching the speaker, then strike it. + // because responses are dispatched from inside a loop that is already walking through the + // queue, it's not safe to actually remove the elements. Instead, quash it by replacing it + // with a null event. + + for ( QueueType_t::IndexLocalType_t idx = m_Queue.Head() ; + idx != m_Queue.InvalidIndex() ; + idx = m_Queue.Next(idx) ) // is now the next index + { + CDeferredResponse &response = m_Queue[idx]; + if ( response.m_Target.m_hHandle.Get() == pSpeaker ) + { + response.Quash(); + } + } +} + +// TODO: use a more compact representation. +void CResponseQueue::DeferContextsFromCriteriaSet( DeferredContexts_t &contextsOut, const AI_CriteriaSet * RESTRICT criteriaIn ) +{ + contextsOut.Reset(); + if (criteriaIn) + { + contextsOut.Merge(criteriaIn); + } +} + +void CResponseQueue::PerFrameDispatch() +{ +failsafe: + // Walk through the list, find any messages whose time has come, and dispatch them. Then remove them. + QueueType_t::IndexLocalType_t idx = m_Queue.Head(); + while (idx != m_Queue.InvalidIndex()) + { + // do we need to dispatch this concept? + CDeferredResponse &response = m_Queue[idx]; + QueueType_t::IndexLocalType_t previdx = idx; // advance the index immediately because we may be deleting the "current" element + idx = m_Queue.Next(idx); // is now the next index + + if ( response.IsQuashed() ) + { + // we can delete this entry now + m_Queue.Remove(previdx); + } + else if ( response.m_fDispatchTime <= gpGlobals->curtime ) + { + // dispatch. we've had bugs where dispatches removed things from inside the queue; + // so, as a failsafe, if the queue length changes as a result, start over. + int oldLength = m_Queue.Count(); + DispatchOneResponse(response); + if ( m_Queue.Count() < oldLength ) + { + AssertMsg( false, "Response queue length changed in non-reentrant way! FAILSAFE TRIGGERED" ); + goto failsafe; // ick + } + + // we can delete this entry now + m_Queue.Remove(previdx); + } + } +} + + +/// Add an expressor owner to this queue. +void CResponseQueue::AddExpresserHost(CBaseEntity *host) +{ + EHANDLE ehost(host); + // see if it's in there already + if (m_ExpresserTargets.HasElement(ehost)) + { + AssertMsg1(false, "Tried to add %s to response queue when it was already in there.", host->GetDebugName()); + } + else + { + // zip through the queue front to back, first see if there's any invalid handles to replace + int count = m_ExpresserTargets.Count(); + for (int i = 0 ; i < count ; ++i ) + { + if ( !m_ExpresserTargets[i].Get() ) + { + m_ExpresserTargets[i] = ehost; + return; + } + } + + // if we're down here we didn't find one to replace, so append the host to the end. + m_ExpresserTargets.AddToTail(ehost); + } +} + +/// Remove an expresser host from this queue. +void CResponseQueue::RemoveExpresserHost(CBaseEntity *host) +{ + int idx = m_ExpresserTargets.Find(host); + if (idx == -1) + { + // AssertMsg1(false, "Tried to remove %s from response queue, but it's not in there to begin with!", host->GetDebugName() ); + } + else + { + m_ExpresserTargets.FastRemove(idx); + } +} + +/// 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) +{ + if ( CBaseMultiplayerPlayer *pPlayer = dynamic_cast(pEnt) ) + { + return pPlayer->GetExpresser(); + } + else if ( CAI_BaseActor *pActor = dynamic_cast(pEnt) ) + { + return pActor->GetExpresser(); + } + //else if ( CFlexExpresser *pFlex = dynamic_cast(pEnt) ) + //{ + // return pFlex->GetExpresser(); + //} + else + { + return NULL; + } +} + + +void CResponseQueue::CDeferredResponse::Quash() +{ + m_Target = CFollowupTargetSpec_t(); + m_fDispatchTime = 0; +} + +bool CResponseQueue::DispatchOneResponse(CDeferredResponse &response) +{ + // find the target. + CBaseEntity * RESTRICT pTarget = NULL; + AI_CriteriaSet &deferredCriteria = response.m_contexts; + CAI_Expresser * RESTRICT pEx = NULL; + CBaseEntity * RESTRICT pIssuer = response.m_hIssuer.Get(); // MAY BE NULL + float followupMaxDistSq; + { + //CFlexExpresser * RESTRICT pOrator = CFlexExpresser::AsFlexExpresser( pIssuer ); + //if ( pOrator ) + //{ + // // max dist is overridden. "0" means infinite distance (for orators only), + // // anything else is a finite distance. + // if ( pOrator->m_flThenAnyMaxDist > 0 ) + // { + // followupMaxDistSq = pOrator->m_flThenAnyMaxDist * pOrator->m_flThenAnyMaxDist; + // } + // else + // { + // followupMaxDistSq = FLT_MAX; + // } + // + //} + //else + { + followupMaxDistSq = rr_followup_maxdist.GetFloat(); // square of max audibility distance + followupMaxDistSq *= followupMaxDistSq; + } + } + + switch (response.m_Target.m_iTargetType) + { + case kDRT_SPECIFIC: + { + pTarget = response.m_Target.m_hHandle.Get(); + } + break; + case kDRT_ANY: + { + return DispatchOneResponse_ThenANY( response, &deferredCriteria, pIssuer, followupMaxDistSq ); + } + break; + case kDRT_ALL: + { + bool bSaidAnything = false; + Vector issuerLocation; + if ( pIssuer ) + { + issuerLocation = pIssuer->GetAbsOrigin(); + } + + // find all characters + int numExprs = GetNumExpresserTargets(); + for ( int i = 0 ; i < numExprs; ++i ) + { + pTarget = GetExpresserHost(i); + float distIssuerToTargetSq = 0.0f; + if ( pIssuer ) + { + distIssuerToTargetSq = (pTarget->GetAbsOrigin() - issuerLocation).LengthSqr(); + if ( distIssuerToTargetSq > followupMaxDistSq ) + continue; // too far + + int iRelation = g_pGameRules->PlayerRelationship(pIssuer, pTarget); + if (iRelation != GR_TEAMMATE && iRelation != GR_ALLY) + continue; + } + + pEx = InferExpresserFromBaseEntity(pTarget); + if ( !pEx || pTarget == pIssuer ) + continue; + AI_CriteriaSet characterCriteria; + pEx->GatherCriteria(&characterCriteria, response.m_concept, NULL); + characterCriteria.Merge(&deferredCriteria); + if ( pIssuer ) + { + characterCriteria.AppendCriteria( "dist_from_issuer", UTIL_VarArgs( "%f", sqrt(distIssuerToTargetSq) ) ); + } + AI_Response prospectiveResponse; + if ( pEx->FindResponse( prospectiveResponse, response.m_concept, &characterCriteria ) ) + { + // dispatch it + bSaidAnything = pEx->SpeakDispatchResponse(response.m_concept, &prospectiveResponse, &deferredCriteria) || bSaidAnything ; + } + } + + return bSaidAnything; + + } + break; + default: + // WTF? + AssertMsg1( false, "Unknown deferred response type %d\n", response.m_Target.m_iTargetType ); + return false; + } + + if (!pTarget) + return false; // we're done right here. + + // Get the expresser for the target. + pEx = InferExpresserFromBaseEntity(pTarget); + if (!pEx) + return false; + + + AI_CriteriaSet characterCriteria; + pEx->GatherCriteria(&characterCriteria, response.m_concept, NULL); + characterCriteria.Merge(&deferredCriteria); + pEx->Speak( response.m_concept, &characterCriteria ); + + return true; +} + +// +ConVar rr_thenany_score_slop( "rr_thenany_score_slop", "0.0", FCVAR_CHEAT, "When computing respondents for a 'THEN ANY' rule, all rule-matching scores within this much of the best score will be considered." ); +#define EXARRAYMAX 32 // maximum number of prospective expressers in the array (hardcoded for simplicity) +bool CResponseQueue::DispatchOneResponse_ThenANY( CDeferredResponse &response, AI_CriteriaSet * RESTRICT pDeferredCriteria, CBaseEntity * const RESTRICT pIssuer, float followupMaxDistSq ) +{ + CBaseEntity * RESTRICT pTarget = NULL; + CAI_Expresser * RESTRICT pEx = NULL; + float bestScore = 0; + float slop = rr_thenany_score_slop.GetFloat(); + Vector issuerLocation; + if ( pIssuer ) + { + issuerLocation = pIssuer->GetAbsOrigin(); + } + + // this is an array of prospective respondents. + CAI_Expresser * RESTRICT pBestEx[EXARRAYMAX]; + AI_Response responseToSay[EXARRAYMAX]; + int numExFound = 0; // and this is the high water mark for the array. + + // Here's the algorithm: we're going to walk through all the characters, finding the + // highest scoring ones for this rule. Let the highest score be called k. + // Because there may be (n) many characters all scoring k, we store an array of + // all characters with score k, then choose randomly from that array at return. + // We also define an allowable error for k in the global cvar + // rr_thenany_score_slop , which may be zero. + + // find all characters (except the issuer) + int numExprs = GetNumExpresserTargets(); + AssertMsg1( numExprs <= EXARRAYMAX, "Response queue has %d possible expresser targets, please increase EXARRAYMAX ", numExprs ); + for ( int i = 0 ; i < numExprs; ++i ) + { + pTarget = GetExpresserHost(i); + if ( pTarget == pIssuer ) + continue; // don't dispatch to myself + + if ( !pTarget->IsAlive() ) + continue; // dead men tell no tales + + float distIssuerToTargetSq = 0.0f; + if ( pIssuer ) + { + distIssuerToTargetSq = (pTarget->GetAbsOrigin() - issuerLocation).LengthSqr(); + if ( distIssuerToTargetSq > followupMaxDistSq ) + continue; // too far + + int iRelation = g_pGameRules->PlayerRelationship(pIssuer, pTarget); + if (iRelation != GR_TEAMMATE && iRelation != GR_ALLY) + continue; + } + + pEx = InferExpresserFromBaseEntity(pTarget); + if ( !pEx ) + continue; + + AI_CriteriaSet characterCriteria; + pEx->GatherCriteria(&characterCriteria, response.m_concept, NULL); + characterCriteria.Merge( pDeferredCriteria ); + pTarget->ModifyOrAppendDerivedCriteria( characterCriteria ); + if ( pIssuer ) + { + characterCriteria.AppendCriteria( "dist_from_issuer", UTIL_VarArgs( "%f", sqrt(distIssuerToTargetSq) ) ); + } + AI_Response prospectiveResponse; + + if ( pEx->FindResponse( prospectiveResponse, response.m_concept, &characterCriteria ) ) + { + float score = prospectiveResponse.GetMatchScore(); + if ( score > 0 && !prospectiveResponse.IsEmpty() ) // ignore scores that are zero, regardless of slop + { + // if this score is better than all we've seen (outside the slop), then replace the array with + // an entry just to this expresser + if ( score > bestScore + slop ) + { + responseToSay[0] = prospectiveResponse; + pBestEx[0] = pEx; + bestScore = score; + numExFound = 1; + } + else if ( score >= bestScore - slop ) // if this score is at least as good as the best we've seen, but not better than all + { + if ( numExFound >= EXARRAYMAX ) + continue; // SAFETY: don't overflow the array + + responseToSay[numExFound] = prospectiveResponse; + pBestEx[numExFound] = pEx; + bestScore = fpmax( score, bestScore ); + numExFound += 1; + } + } + } + } + + // if I have a response, dispatch it. + if ( numExFound > 0 ) + { + // get a random number between 0 and the responses found + int iSelect = numExFound > 1 ? RandomInt( 0, numExFound - 1 ) : 0; + + if ( pBestEx[iSelect] != NULL ) + { + return pBestEx[iSelect]->SpeakDispatchResponse( response.m_concept, responseToSay + iSelect, pDeferredCriteria ); + } + else + { + AssertMsg( false, "Response queue somehow found a response, but no expresser for it.\n" ); + return false; + } + } + else + { // I did not find a response. + return false; + } + + return false; // just in case +} + +void CResponseQueue::Evacuate() +{ + m_Queue.RemoveAll(); +} + +#undef EXARRAYMAX + + +/////////////////////////////////////////////////////////////////////////////// +// RESPONSE QUEUE MANAGER +/////////////////////////////////////////////////////////////////////////////// + + +void CResponseQueueManager::LevelInitPreEntity( void ) +{ + if (m_pQueue == NULL) + { + m_pQueue = new CResponseQueue(AI_RESPONSE_QUEUE_SIZE); + } +} + +CResponseQueueManager::~CResponseQueueManager() +{ + if (m_pQueue != NULL) + { + delete m_pQueue; + m_pQueue = NULL; + } +} + +void CResponseQueueManager::Shutdown() +{ + if (m_pQueue != NULL) + { + delete m_pQueue; + m_pQueue = NULL; + } +} + +void CResponseQueueManager::FrameUpdatePostEntityThink() +{ + Assert(m_pQueue); + m_pQueue->PerFrameDispatch(); +} + +CResponseQueueManager g_ResponseQueueManager( "CResponseQueueManager" ); + diff --git a/src/game/server/ai_speechqueue.h b/src/game/server/ai_speechqueue.h new file mode 100644 index 000000000..15101b70b --- /dev/null +++ b/src/game/server/ai_speechqueue.h @@ -0,0 +1,239 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: An event queue of AI concepts that dispatches them to appropriate characters. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef AI_SPEECHQUEUE_H +#define AI_SPEECHQUEUE_H + +#if defined( _WIN32 ) +#pragma once +#endif + +#include "ai_speech.h" + +#define AI_RESPONSE_QUEUE_SIZE 64 + +enum DeferredResponseTarget_t // possible targets for a deferred response +{ + kDRT_ANY, // best matching respondent within range -- except for the one in the m_hTarget handle + kDRT_ALL, // send to everyone in range -- except for the one in the m_hTarget handle + kDRT_SPECIFIC, // a specific entity is targeted + + kDRT_MAX, // high water mark +}; + +// Allows you to postpone AI speech concepts to a later time, or to direct them to +// a specific character, or all of them. +class CResponseQueue +{ + //////////////////// Local types //////////////////// +public: + + // We pack up contexts to send along with the concept. + // For now I'll just copy criteria sets, but it will be better to do something + // more efficient in the future. + typedef AI_CriteriaSet DeferredContexts_t; + + struct CFollowupTargetSpec_t ///< to whom a followup is directed. Can be a specific entity or something more exotic. + { + DeferredResponseTarget_t m_iTargetType; ///< ANY, ALL, or SPECIFIC. If specific, pass through a handle to: + EHANDLE m_hHandle; ///< a specific target for the message, or a specific character to OMIT. + inline bool IsValid( void ) const; + + // constructors/destructors + explicit CFollowupTargetSpec_t(const DeferredResponseTarget_t &targetType, const EHANDLE &handle) + : m_iTargetType(targetType), m_hHandle(handle) + {}; + explicit CFollowupTargetSpec_t(const EHANDLE &handle) + : m_iTargetType(kDRT_SPECIFIC), m_hHandle(handle) + {}; + CFollowupTargetSpec_t(DeferredResponseTarget_t target) // eg, ANY, ALL, etc. + : m_iTargetType(target) + { + AssertMsg(m_iTargetType != kDRT_SPECIFIC, "Response rule followup tried to specify an entity target, but didn't provide the target.\n" ); + } + CFollowupTargetSpec_t(void) // default: invalid + : m_iTargetType(kDRT_MAX) + {}; + }; + + /// A single deferred response. + struct CDeferredResponse + { + AIConcept_t m_concept; + DeferredContexts_t m_contexts; ///< contexts to send along with the concept + float m_fDispatchTime; + EHANDLE m_hIssuer; ///< an entity, if issued by an entity + /* + DeferredResponseTarget_t m_iTargetType; + EHANDLE m_hTarget; // May be invalid. + */ + CFollowupTargetSpec_t m_Target; + + inline void Init( const AIConcept_t &concept, const AI_CriteriaSet * RESTRICT contexts, float dtime, const CFollowupTargetSpec_t &target, CBaseEntity *pIssuer ); + inline bool IsQuashed() { return !m_Target.IsValid(); } + void Quash(); ///< make this response invalid. + }; + /// write + static void DeferContextsFromCriteriaSet( DeferredContexts_t &contextsOut, const AI_CriteriaSet *criteriaIn ); + + //////////////////// Methods //////////////////// +public: + CResponseQueue( int queueSize ); + + /// Add a deferred response. + void Add( const AIConcept_t &concept, ///< concept to dispatch + const AI_CriteriaSet * RESTRICT contexts, ///< the contexts that come with it (may be NULL) + float time, ///< when to dispatch it. You can specify a time of zero to mean "immediately." + const CFollowupTargetSpec_t &targetspec, /// All information necessary to target this response + CBaseEntity *pIssuer = NULL ///< the entity who should not respond if this is a ANY or ALL rule. (eg, don't let people talk to themselves.) + ); + + /// Remove all deferred responses matching the concept and issuer. + void Remove( const AIConcept_t &concept, ///< concept to dispatch + CBaseEntity * const pIssuer = NULL ///< the entity issuing the response, if one exists. + ); + + /// Remove all deferred responses queued to be spoken by given character + void RemoveSpeechQueuedFor( const CBaseEntity *pSpeaker ); + + /// Empty out all pending events + void Evacuate(); + + /// Go through and dispatch any deferred responses. + void PerFrameDispatch(); + + /// Add an expressor owner to this queue. + void AddExpresserHost(CBaseEntity *host); + + /// Remove an expresser host from this queue. + void RemoveExpresserHost(CBaseEntity *host); + + /// Iterate over potential expressers for this queue + inline int GetNumExpresserTargets() const; + inline CBaseEntity *GetExpresserHost(int which) const; + +protected: + /// Actually send off one response to a consumer + /// Return true if dispatch succeeded + bool DispatchOneResponse( CDeferredResponse &response ); + +private: + /// Helper function for one case in DispatchOneResponse + /// (for better organization) + bool DispatchOneResponse_ThenANY( CDeferredResponse &response, AI_CriteriaSet * RESTRICT pDeferredCriteria, CBaseEntity * const RESTRICT pIssuer, float followupMaxDistSq ); + + //////////////////// Data //////////////////// +protected: + typedef CUtlFixedLinkedList< CDeferredResponse > QueueType_t; + QueueType_t m_Queue; // the queue of deferred responses, will eventually be sorted + /// Note about the queue type: if you move to replace it with a sorted priority queue, + /// make sure it is a type such that an iterator is not invalidated by inserts and deletes. + /// CResponseQueue::PerFrameDispatch() iterates over the queue calling DispatchOneResponse + /// on each in turn, and those responses may very easily add new events to the queue. + /// A crash will result if the iterator used in CResponseQueue::PerFrameDispatch()'s loop + /// becomes invalid. + + CUtlVector m_ExpresserTargets; // a list of legitimate expresser targets +}; + +inline void CResponseQueue::CDeferredResponse::Init(const AIConcept_t &concept, const AI_CriteriaSet * RESTRICT contexts, float dtime, const CFollowupTargetSpec_t &target, CBaseEntity *pIssuer ) +{ + m_concept = concept; + m_fDispatchTime = dtime; + /* + m_iTargetType = targetType; + m_hTarget = handle ; + */ + m_Target = target; + m_hIssuer = pIssuer; + DeferContextsFromCriteriaSet(m_contexts, contexts); +} + +int CResponseQueue::GetNumExpresserTargets() const +{ + return m_ExpresserTargets.Count(); +} + +CBaseEntity *CResponseQueue::GetExpresserHost(int which) const +{ + return m_ExpresserTargets[which]; +} + + +// The wrapper game system that contains a response queue, and ticks it each frame. + +class CResponseQueueManager : public CAutoGameSystemPerFrame +{ +public: + CResponseQueueManager(char const *name) : CAutoGameSystemPerFrame( name ) + { + m_pQueue = NULL; + } + virtual ~CResponseQueueManager(void); + virtual void Shutdown(); + virtual void FrameUpdatePostEntityThink( void ); + virtual void LevelInitPreEntity( void ); + + inline CResponseQueue *GetQueue(void) { Assert(m_pQueue); return m_pQueue; } + +protected: + CResponseQueue *m_pQueue; +}; + + +// Valid if the target type enum is within bounds. Furthermore if it +// specifies a specific entity, that handle must be valid. +bool CResponseQueue::CFollowupTargetSpec_t::IsValid( void ) const +{ + if (m_iTargetType >= kDRT_MAX) + return false; + if (m_iTargetType < 0) + return false; + if (m_iTargetType == kDRT_SPECIFIC && !m_hHandle.IsValid()) + return false; + + return true; +} + +extern CResponseQueueManager g_ResponseQueueManager; + + +// Handy global helper funcs + +/// Automatically queue up speech to happen immediately -- calls straight through to response rules add +inline void QueueSpeak( const AIConcept_t &concept, ///< concept name to say + const CResponseQueue::CFollowupTargetSpec_t& targetspec, ///< kDRT_ANY, kDRT_ALL, etc + CBaseEntity *pIssuer = NULL ///< if specifying ANY or ALL, use this to specify the one you *don't* want to speak + ) +{ + return g_ResponseQueueManager.GetQueue()->Add( concept, NULL, 0.0f, targetspec, pIssuer ); +} + +/// Automatically queue up speech to happen immediately -- calls straight through to response rules add +inline void QueueSpeak( const AIConcept_t &concept, ///< concept name to say + const CResponseQueue::CFollowupTargetSpec_t& targetspec, ///< kDRT_ANY, kDRT_ALL, etc + const AI_CriteriaSet &criteria, ///< criteria to pass in + CBaseEntity *pIssuer = NULL ///< if specifying ANY or ALL, use this to specify the one you *don't* want to speak + ) +{ + return g_ResponseQueueManager.GetQueue()->Add( concept, &criteria, 0.0f, targetspec, pIssuer ); +} + +/// Automatically queue up speech to happen immediately -- calls straight through to response rules add +inline void QueueSpeak( const AIConcept_t &concept, ///< concept name to say + const EHANDLE &target, ///< which entity shall speak + float delay, ///< how far in the future to speak + const AI_CriteriaSet &criteria, ///< criteria to pass in + CBaseEntity *pIssuer = NULL ) +{ + return g_ResponseQueueManager.GetQueue()->Add( concept, &criteria, gpGlobals->curtime + delay, + CResponseQueue::CFollowupTargetSpec_t(target), pIssuer ); +} + + + +#endif // AI_SPEECHQUEUE_H diff --git a/src/game/server/baseentity.cpp b/src/game/server/baseentity.cpp index 747f54ca2..b387228b0 100644 --- a/src/game/server/baseentity.cpp +++ b/src/game/server/baseentity.cpp @@ -6607,35 +6607,72 @@ void CBaseEntity::InputFireUser4( inputdata_t& inputdata ) //----------------------------------------------------------------------------- void CBaseEntity::AddContext( const char *contextName ) { - char key[ 128 ]; - char value[ 128 ]; + char key[128]; + char value[128]; float duration; - const char *p = contextName; - while ( p ) + + const char* p = contextName; + while (p) { duration = 0.0f; - p = SplitContext( p, key, sizeof( key ), value, sizeof( value ), &duration ); - if ( duration ) + p = SplitContext(p, key, sizeof(key), value, sizeof(value), &duration, contextName); + if (duration) { duration += gpGlobals->curtime; } - int iIndex = FindContextByName( key ); - if ( iIndex != -1 ) + + AddContext(key, value, duration); + + } +} + +#include "ai_speech.h" +//----------------------------------------------------------------------------- +// Purpose: add exactly one context key,value pair to this object +// Input : inputdata - +//----------------------------------------------------------------------------- +void CBaseEntity::AddContext(const char* pKey, const char* pValue, float duration) +{ + int iIndex = FindContextByName(pKey); + if (iIndex != -1) + { + // Set the existing context to the new value + char buf[64]; + if (RR::CApplyContextOperator::FindOperator(pValue)->Apply( + m_ResponseContexts[iIndex].m_iszValue.ToCStr(), pValue, buf, sizeof(buf))) { - // Set the existing context to the new value - m_ResponseContexts[iIndex].m_iszValue = AllocPooledString( value ); - m_ResponseContexts[iIndex].m_fExpirationTime = duration; - continue; + m_ResponseContexts[iIndex].m_iszValue = AllocPooledString(buf); + } + else + { + Warning("RR: could not apply operator %s to prior value %s\n", + pValue, m_ResponseContexts[iIndex].m_iszValue.ToCStr()); + m_ResponseContexts[iIndex].m_iszValue = AllocPooledString(pValue); } + m_ResponseContexts[iIndex].m_fExpirationTime = duration; + } + else + { ResponseContext_t newContext; - newContext.m_iszName = AllocPooledString( key ); - newContext.m_iszValue = AllocPooledString( value ); + newContext.m_iszName = AllocPooledString(pKey); + + // Create a new context with the appropriate value ( some operators assume 0 on nonexistent prior ) + char buf[64]; + if (RR::CApplyContextOperator::FindOperator(pValue)->Apply( + NULL, pValue, buf, sizeof(buf))) + { + newContext.m_iszValue = AllocPooledString(buf); + } + else + { + newContext.m_iszValue = AllocPooledString(pValue); + } newContext.m_fExpirationTime = duration; - m_ResponseContexts.AddToTail( newContext ); + m_ResponseContexts.AddToTail(newContext); } } @@ -6666,7 +6703,7 @@ void CBaseEntity::InputClearContext( inputdata_t& inputdata ) // Purpose: // Output : IResponseSystem //----------------------------------------------------------------------------- -IResponseSystem *CBaseEntity::GetResponseSystem() +ResponseRules::IResponseSystem *CBaseEntity::GetResponseSystem() { return NULL; } @@ -6728,6 +6765,8 @@ void CBaseEntity::InputAddOutput( inputdata_t &inputdata ) //----------------------------------------------------------------------------- void CBaseEntity::DispatchResponse( const char *conceptName ) { + using namespace ResponseRules; + IResponseSystem *rs = GetResponseSystem(); if ( !rs ) return; @@ -6750,35 +6789,45 @@ void CBaseEntity::DispatchResponse( const char *conceptName ) return; // Handle the response here... - const char *szResponse = result.GetResponsePtr(); - switch ( result.GetType() ) + char response[256]; + result.GetResponse(response, sizeof(response)); + switch (result.GetType()) { - case RESPONSE_SPEAK: - EmitSound( szResponse ); - break; - - case RESPONSE_SENTENCE: + case ResponseRules::RESPONSE_SPEAK: + { + EmitSound(response); + } + break; + case ResponseRules::RESPONSE_SENTENCE: + { + int sentenceIndex = SENTENCEG_Lookup(response); + if (sentenceIndex == -1) { - int sentenceIndex = SENTENCEG_Lookup( szResponse ); - if( sentenceIndex == -1 ) - { - // sentence not found - break; - } - - // FIXME: Get pitch from npc? - CPASAttenuationFilter filter( this ); - CBaseEntity::EmitSentenceByIndex( filter, entindex(), CHAN_VOICE, sentenceIndex, 1, result.GetSoundLevel(), 0, PITCH_NORM ); + // sentence not found + break; } - break; - case RESPONSE_SCENE: + // FIXME: Get pitch from npc? + CPASAttenuationFilter filter(this); + CBaseEntity::EmitSentenceByIndex(filter, entindex(), CHAN_VOICE, sentenceIndex, 1, result.GetSoundLevel(), 0, PITCH_NORM); + } + break; + case ResponseRules::RESPONSE_SCENE: + { // Try to fire scene w/o an actor - InstancedScriptedScene( NULL, szResponse ); - break; + InstancedScriptedScene(NULL, response); + } + break; + case ResponseRules::RESPONSE_PRINT: + { - case RESPONSE_PRINT: + } + break; + case ResponseRules::RESPONSE_ENTITYIO: + { + CAI_Expresser::FireEntIOFromResponse(response, this); break; + } default: // Don't know how to handle .vcds!!! break; diff --git a/src/game/server/baseentity.h b/src/game/server/baseentity.h index 7a6af3ab6..f39606577 100644 --- a/src/game/server/baseentity.h +++ b/src/game/server/baseentity.h @@ -20,14 +20,14 @@ #include "ServerNetworkProperty.h" #include "shareddefs.h" #include "engine/ivmodelinfo.h" +#include "AI_Criteria.h" +#include "AI_ResponseSystem.h" class CDamageModifier; class CDmgAccumulator; struct CSoundParameters; -class AI_CriteriaSet; -class IResponseSystem; class IEntitySaveUtils; class CRecipientFilter; class CStudioHdr; @@ -911,6 +911,7 @@ class CBaseEntity : public IServerEntity int FindContextByName( const char *name ) const; public: void AddContext( const char *nameandvalue ); + void AddContext(const char* pKey, const char* pValue, float duration); protected: CUtlVector< ResponseContext_t > m_ResponseContexts; @@ -939,7 +940,7 @@ class CBaseEntity : public IServerEntity virtual CBaseAnimating* GetBaseAnimating() { return 0; } virtual CBaseAnimatingOverlay* GetBaseAnimatingOverlay() { return 0; } - virtual IResponseSystem *GetResponseSystem(); + virtual ResponseRules::IResponseSystem *GetResponseSystem(); virtual void DispatchResponse( const char *conceptName ); // Classify - returns the type of group (i.e, "houndeye", or "human military" so that NPCs with different classnames @@ -1188,6 +1189,10 @@ class CBaseEntity : public IServerEntity #endif // _DEBUG virtual void ModifyOrAppendCriteria( AI_CriteriaSet& set ); + // this computes criteria that depend on the other criteria having been set. + // needs to be done in a second pass because we may have multiple overrids for + // a context before it all settles out. + virtual void ModifyOrAppendDerivedCriteria(AI_CriteriaSet& set) {}; void AppendContextToCriteria( AI_CriteriaSet& set, const char *prefix = "" ); void DumpResponseCriteria( void ); diff --git a/src/game/server/baseflex.h b/src/game/server/baseflex.h index f7b407c4e..73a2cb24b 100644 --- a/src/game/server/baseflex.h +++ b/src/game/server/baseflex.h @@ -19,7 +19,6 @@ struct flexsettinghdr_t; struct flexsetting_t; -class AI_Response; //----------------------------------------------------------------------------- // Purpose: A .vfe referenced by a scene during .vcd playback diff --git a/src/game/server/basemultiplayerplayer.cpp b/src/game/server/basemultiplayerplayer.cpp index fee7515db..9024dc3f8 100644 --- a/src/game/server/basemultiplayerplayer.cpp +++ b/src/game/server/basemultiplayerplayer.cpp @@ -12,6 +12,8 @@ // Minimum interval between rate-limited commands that players can run. #define COMMAND_MAX_RATE 0.3 +using namespace ResponseRules; + CBaseMultiplayerPlayer::CBaseMultiplayerPlayer() { m_iCurrentConcept = MP_CONCEPT_NONE; @@ -28,6 +30,7 @@ CBaseMultiplayerPlayer::CBaseMultiplayerPlayer() CBaseMultiplayerPlayer::~CBaseMultiplayerPlayer() { m_pAchievementKV->deleteThis(); + delete m_pExpresser; } //----------------------------------------------------------------------------- @@ -91,7 +94,9 @@ bool CBaseMultiplayerPlayer::SpeakConcept( AI_Response &response, int iConcept ) { // Save the current concept. m_iCurrentConcept = iConcept; - return SpeakFindResponse( response, g_pszMPConcepts[iConcept] ); + CAI_Concept concept(g_pszMPConcepts[iConcept]); + concept.SetSpeaker(this); + return FindResponse( response, concept ); } //----------------------------------------------------------------------------- diff --git a/src/game/server/basemultiplayerplayer.h b/src/game/server/basemultiplayerplayer.h index a1ea58f8b..aacded6ff 100644 --- a/src/game/server/basemultiplayerplayer.h +++ b/src/game/server/basemultiplayerplayer.h @@ -27,7 +27,7 @@ class CBaseMultiplayerPlayer : public CAI_ExpresserHost virtual void ModifyOrAppendCriteria( AI_CriteriaSet& criteriaSet ); virtual bool SpeakIfAllowed( AIConcept_t concept, const char *modifiers = NULL, char *pszOutResponseChosen = NULL, size_t bufsize = 0, IRecipientFilter *filter = NULL ); - virtual IResponseSystem *GetResponseSystem(); + virtual ResponseRules::IResponseSystem *GetResponseSystem(); bool SpeakConcept( AI_Response& response, int iConcept ); virtual bool SpeakConceptIfAllowed( int iConcept, const char *modifiers = NULL, char *pszOutResponseChosen = NULL, size_t bufsize = 0, IRecipientFilter *filter = NULL ); diff --git a/src/game/server/bms/npc_basecollegue.cpp b/src/game/server/bms/npc_basecollegue.cpp index 78cb68ade..880e0eaa2 100644 --- a/src/game/server/bms/npc_basecollegue.cpp +++ b/src/game/server/bms/npc_basecollegue.cpp @@ -4,7 +4,7 @@ #include "sceneentity.h" #include "npc_basecollegue.h" - +using namespace ResponseRules; #define ALYX_FEAR_ZOMBIE_DIST_SQR Square(60) @@ -264,9 +264,9 @@ void CNPC_BaseColleague::UseFunc(CBaseEntity *pActivator, CBaseEntity *pCaller, if (IsAllowedToSpeak(TLK_VITALIDLE, true)) { AI_Response pResp; - if (SpeakFindResponse(pResp, TLK_VITALIDLE) && pResp.GetType() != RESPONSE_NONE) + if (SpeakFindResponse(pResp, TLK_VITALIDLE) && pResp.GetType() != ResponseRules::RESPONSE_NONE) { - bSpoke = SpeakDispatchResponse(TLK_VITALIDLE, pResp); + bSpoke = SpeakDispatchResponse(TLK_VITALIDLE, &pResp); } } diff --git a/src/game/server/hl1/hl1_npc_scientist.cpp b/src/game/server/hl1/hl1_npc_scientist.cpp index 2265649eb..aaf2141fd 100644 --- a/src/game/server/hl1/hl1_npc_scientist.cpp +++ b/src/game/server/hl1/hl1_npc_scientist.cpp @@ -1073,8 +1073,8 @@ void CNPC_SittingScientist::SittingThink( void ) ResetSequenceInfo( ); SetCycle( 0 ); SetBoneController( 0, 0 ); - - GetExpresser()->Speak( TLK_HELLO ); + CAI_Concept concept(TLK_HELLO); + GetExpresser()->Speak(concept); } } else if ( IsSequenceFinished() ) @@ -1085,7 +1085,8 @@ void CNPC_SittingScientist::SittingThink( void ) if (m_flResponseDelay && gpGlobals->curtime > m_flResponseDelay) { // respond to question - GetExpresser()->Speak( TLK_QUESTION ); + CAI_Concept concept(TLK_QUESTION); + GetExpresser()->Speak(concept); SetSequence( m_baseSequence + SITTING_ANIM_sitscared ); m_flResponseDelay = 0; } diff --git a/src/game/server/hl1/hl1_npc_talker.h b/src/game/server/hl1/hl1_npc_talker.h index a417e5fb7..60f0334d5 100644 --- a/src/game/server/hl1/hl1_npc_talker.h +++ b/src/game/server/hl1/hl1_npc_talker.h @@ -69,7 +69,7 @@ class CHL1NPCTalker : public CNPCSimpleTalker Disposition_t IRelationType( CBaseEntity *pTarget ); - virtual IResponseSystem *GetResponseSystem() { return m_pInstancedResponseSystem; } + virtual ResponseRules::IResponseSystem *GetResponseSystem() { return m_pInstancedResponseSystem; } void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); @@ -97,7 +97,7 @@ class CHL1NPCTalker : public CNPCSimpleTalker protected: virtual void FollowerUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); - IResponseSystem *m_pInstancedResponseSystem; + ResponseRules::IResponseSystem *m_pInstancedResponseSystem; private: virtual void DeclineFollowing( void ) {} diff --git a/src/game/server/hl2/ai_behavior_actbusy.cpp b/src/game/server/hl2/ai_behavior_actbusy.cpp index 5b233aa57..b2e645bea 100644 --- a/src/game/server/hl2/ai_behavior_actbusy.cpp +++ b/src/game/server/hl2/ai_behavior_actbusy.cpp @@ -1646,7 +1646,7 @@ void CAI_ActBusyBehavior::PlaySoundForActBusy( busyanimparts_t AnimPart ) CAI_Expresser *pExpresser = GetOuter()->GetExpresser(); if ( pExpresser ) { - const char *concept = STRING(pBusyAnim->iszSounds[AnimPart]); + CAI_Concept concept = STRING(pBusyAnim->iszSounds[AnimPart]); // Must be able to speak the concept if ( !pExpresser->IsSpeaking() && pExpresser->CanSpeakConcept( concept ) ) diff --git a/src/game/server/hl2/env_speaker.h b/src/game/server/hl2/env_speaker.h index 20fbeb6b5..b6b1820b7 100644 --- a/src/game/server/hl2/env_speaker.h +++ b/src/game/server/hl2/env_speaker.h @@ -27,7 +27,7 @@ class CSpeaker : public CPointEntity virtual int ObjectCaps( void ) { return (BaseClass::ObjectCaps() & ~FCAP_ACROSS_TRANSITION); } - virtual IResponseSystem *GetResponseSystem() { return m_pInstancedResponseSystem; } + virtual ResponseRules::IResponseSystem *GetResponseSystem() { return m_pInstancedResponseSystem; } virtual int Save( ISave &save ); virtual int Restore( IRestore &restore ); @@ -43,7 +43,7 @@ class CSpeaker : public CPointEntity string_t m_iszRuleScriptFile; string_t m_iszConcept; - IResponseSystem *m_pInstancedResponseSystem; + ResponseRules::IResponseSystem *m_pInstancedResponseSystem; public: diff --git a/src/game/server/peter/laz_player.cpp b/src/game/server/peter/laz_player.cpp index e39f83251..78c0edd32 100644 --- a/src/game/server/peter/laz_player.cpp +++ b/src/game/server/peter/laz_player.cpp @@ -33,6 +33,7 @@ // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" +using namespace ResponseRules; #define HL2MP_COMMAND_MAX_RATE 0.3 @@ -1037,7 +1038,7 @@ void CLaz_Player::AnswerQuestion(CBaseCombatCharacter* pQuestioner, int iQARando } } - SpeakDispatchResponse("TLK_PLAYER_ANSWER", selection); + SpeakDispatchResponse("TLK_PLAYER_ANSWER", &selection); } else if (rr_debug_qa.GetBool()) { diff --git a/src/game/server/physconstraint.cpp b/src/game/server/physconstraint.cpp index 210e569c8..887b7801c 100644 --- a/src/game/server/physconstraint.cpp +++ b/src/game/server/physconstraint.cpp @@ -572,7 +572,7 @@ void CPhysConstraint::GetConstraintObjects( hl_constraint_info_t &info ) if ( Q_strlen(STRING(m_nameAttach1)) ) { Warning("Bogus constraint %s (attaches ENTITY NOT FOUND:%s to %s)\n", GetDebugName(), STRING(m_nameAttach1), STRING(m_nameAttach2)); -#ifdef HL2_EPISODIC +#if defined(HL2_EPISODIC) && !defined(HL2_LAZUL) info.pObjects[0] = info.pObjects[1] = NULL; return; #endif // HL2_EPISODIC @@ -585,7 +585,7 @@ void CPhysConstraint::GetConstraintObjects( hl_constraint_info_t &info ) if ( Q_strlen(STRING(m_nameAttach2)) ) { Warning("Bogus constraint %s (attaches %s to ENTITY NOT FOUND:%s)\n", GetDebugName(), STRING(m_nameAttach1), STRING(m_nameAttach2)); -#ifdef HL2_EPISODIC +#if defined(HL2_EPISODIC) && !defined(HL2_LAZUL) info.pObjects[0] = info.pObjects[1] = NULL; return; #endif // HL2_EPISODIC diff --git a/src/game/server/physics_prop_ragdoll.cpp b/src/game/server/physics_prop_ragdoll.cpp index bcbb4ef22..51f6ce7bf 100644 --- a/src/game/server/physics_prop_ragdoll.cpp +++ b/src/game/server/physics_prop_ragdoll.cpp @@ -480,9 +480,9 @@ void CRagdollProp::InitRagdollAnimation() //----------------------------------------------------------------------------- // Response system stuff //----------------------------------------------------------------------------- -IResponseSystem *CRagdollProp::GetResponseSystem() +ResponseRules::IResponseSystem *CRagdollProp::GetResponseSystem() { - extern IResponseSystem *g_pResponseSystem; + extern ResponseRules::IResponseSystem *g_pResponseSystem; // Just use the general NPC response system; we often come from NPCs after all return g_pResponseSystem; diff --git a/src/game/server/physics_prop_ragdoll.h b/src/game/server/physics_prop_ragdoll.h index b25627dfb..26691b896 100644 --- a/src/game/server/physics_prop_ragdoll.h +++ b/src/game/server/physics_prop_ragdoll.h @@ -53,7 +53,7 @@ class CRagdollProp : public CBaseAnimating, public CDefaultPlayerPickupVPhysics virtual int DrawDebugTextOverlays(void); // Response system stuff - virtual IResponseSystem *GetResponseSystem(); + virtual ResponseRules::IResponseSystem *GetResponseSystem(); virtual void ModifyOrAppendCriteria( AI_CriteriaSet& set ); void SetSourceClassName( const char *pClassname ); diff --git a/src/game/server/sceneentity.cpp b/src/game/server/sceneentity.cpp index 00d78c242..2b8ee5a63 100644 --- a/src/game/server/sceneentity.cpp +++ b/src/game/server/sceneentity.cpp @@ -2285,15 +2285,19 @@ void CSceneEntity::InputInterjectResponse( inputdata_t &inputdata ) // Try to find the response for this slot. AI_Response response; - bool result = npc->SpeakFindResponse( response, inputdata.value.String(), modifiers.Get() ); + CAI_Concept concept(inputdata.value.String()); + concept.SetSpeaker(npc); + AI_CriteriaSet set; + npc->GatherCriteria(&set, concept, modifiers.Get()); + bool result = npc->FindResponse( response, concept, &set); if ( result ) { - float duration = npc->GetResponseDuration( response ); + float duration = npc->GetResponseDuration( &response ); if ( ( duration > 0.0f ) && npc->PermitResponse( duration ) ) { // If we could look it up, dispatch it and bail. - npc->SpeakDispatchResponse( inputdata.value.String(), response ); + npc->SpeakDispatchResponse( concept, &response, &set); return; } } @@ -2821,10 +2825,11 @@ void CSceneEntity::QueueResumePlayback( void ) if ( pBaseActor ) { AI_Response response; - bool result = pBaseActor->SpeakFindResponse( response, STRING(m_iszResumeSceneFile), NULL ); + CAI_Concept concept(STRING(m_iszResumeSceneFile)); + bool result = pBaseActor->FindResponse( response, concept, NULL ); if ( result ) { - const char *szResponse = response.GetResponsePtr(); + const char* szResponse = response.GetResponsePtr(); bStartedScene = InstancedScriptedScene( NULL, szResponse, &m_hWaitingForThisResumeScene, 0, false ) != 0; } } @@ -4713,6 +4718,34 @@ float GetSceneDuration( char const *pszScene ) return flSecs; } +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pszScene - +// Output : float +//----------------------------------------------------------------------------- +float GetSceneSpeechDuration(char const* pszScene) +{ + float flSecs = 0.0f; + + CChoreoScene* pScene = CSceneEntity::LoadScene(pszScene, nullptr); + if (pScene) + { + for (int i = pScene->GetNumEvents()-1; i >= 0 ; i--) + { + CChoreoEvent* pEvent = pScene->GetEvent(i); + + if (pEvent->GetType() == CChoreoEvent::SPEAK) + { + flSecs = pEvent->GetStartTime() + pEvent->GetDuration(); + break; + } + } + delete pScene; + } + + return flSecs; +} + //----------------------------------------------------------------------------- // Purpose: // Input : *pszScene - diff --git a/src/game/server/sceneentity.h b/src/game/server/sceneentity.h index f1580798b..a4bacadb2 100644 --- a/src/game/server/sceneentity.h +++ b/src/game/server/sceneentity.h @@ -13,7 +13,6 @@ // List of the last 5 lines of speech from NPCs for bug reports #define SPEECH_LIST_MAX_SOUNDS 5 -class AI_Response; struct recentNPCSpeech_t { @@ -36,6 +35,7 @@ bool IsRunningScriptedSceneAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstance bool IsRunningScriptedSceneWithSpeech( CBaseFlex *pActor, bool bIgnoreInstancedScenes = false ); bool IsRunningScriptedSceneWithSpeechAndNotPaused( CBaseFlex *pActor, bool bIgnoreInstancedScenes = false ); float GetSceneDuration( char const *pszScene ); +float GetSceneSpeechDuration(char const* pszScene); int GetSceneSpeechCount( char const *pszScene ); bool IsInInterruptableScenes( CBaseFlex *pActor ); diff --git a/src/game/server/server_base.vpc b/src/game/server/server_base.vpc index c958c8706..eb388f6ad 100644 --- a/src/game/server/server_base.vpc +++ b/src/game/server/server_base.vpc @@ -189,6 +189,7 @@ $Project $File "ai_dynamiclink.cpp" $File "ai_dynamiclink.h" $File "ai_event.cpp" + $File "ai_expresserfollowup.cpp" $File "ai_goalentity.cpp" $File "ai_goalentity.h" $File "ai_hint.cpp" @@ -255,6 +256,8 @@ $Project $File "ai_speech.h" $File "ai_speechfilter.cpp" $File "ai_speechfilter.h" + $File "ai_speechqueue.cpp" + $File "ai_speechqueue.h" $File "ai_squad.cpp" $File "ai_squad.h" $File "ai_squadslot.cpp" @@ -491,7 +494,6 @@ $Project $File "init_factory.h" $File "intermission.cpp" $File "$SRCDIR\public\interpolatortypes.h" - $File "$SRCDIR\game\shared\interval.h" $File "$SRCDIR\public\iregistry.h" $File "$SRCDIR\game\shared\iscenetokenprocessor.h" $File "iservervehicle.h" @@ -761,7 +763,6 @@ $Project "h_export.cpp" \ "init_factory.cpp" \ "$SRCDIR\public\interpolatortypes.cpp" \ - "$SRCDIR\game\shared\interval.cpp" \ "$SRCDIR\public\keyframe\keyframe.cpp" \ "$SRCDIR\common\language.cpp" \ "$SRCDIR\public\map_utils.cpp" \ @@ -1078,6 +1079,7 @@ $Project $File "$SRCDIR\public\zip_uncompressed.h" $File "$SRCDIR\game\shared\mp_shareddefs.h" $File "$SRCDIR\game\shared\econ\ihasowner.h" + $File "$SRCDIR\public\tier1\interval.h" //Haptics $File "$SRCDIR\public\haptics\haptic_utils.h" [$WIN32] } @@ -1098,6 +1100,7 @@ $Project $Lib tier2 $Lib tier3 $Lib fgdlib + $Lib responserules $ImpLibexternal steam_api } } diff --git a/src/game/shared/multiplay_gamerules.h b/src/game/shared/multiplay_gamerules.h index b74dd34fa..4c2580c58 100644 --- a/src/game/shared/multiplay_gamerules.h +++ b/src/game/shared/multiplay_gamerules.h @@ -227,7 +227,7 @@ class CMultiplayRules : public CGameRules struct ResponseRules_t { - CUtlVector m_ResponseSystems; + CUtlVector m_ResponseSystems; }; CUtlVector m_ResponseRules; diff --git a/src/lib/public/responserules.lib b/src/lib/public/responserules.lib new file mode 100644 index 000000000..6e193cd0a Binary files /dev/null and b/src/lib/public/responserules.lib differ diff --git a/src/public/datamap.h b/src/public/datamap.h index 5cc879173..be484292c 100644 --- a/src/public/datamap.h +++ b/src/public/datamap.h @@ -315,6 +315,12 @@ struct datamap_t template friend void DataMapAccess(T *, datamap_t **p); \ template friend datamap_t *DataMapInit(T *); +#define DECLARE_SIMPLE_DATADESC_INSIDE_NAMESPACE() \ + static datamap_t m_DataMap; \ + static datamap_t *GetBaseMap(); \ + template friend void ::DataMapAccess(T *, datamap_t **p); \ + template friend datamap_t *::DataMapInit(T *); + #define DECLARE_DATADESC() \ DECLARE_SIMPLE_DATADESC() \ virtual datamap_t *GetDataDescMap( void ); @@ -415,6 +421,8 @@ inline void DataMapAccess(T *ignored, datamap_t **p) *p = &T::m_DataMap; } +template datamap_t* DataMapInit(T*); + //----------------------------------------------------------------------------- class CDatadescGeneratedNameHolder diff --git a/src/public/responserules/response_host_interface.h b/src/public/responserules/response_host_interface.h new file mode 100644 index 000000000..86eedc896 --- /dev/null +++ b/src/public/responserules/response_host_interface.h @@ -0,0 +1,61 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: Core types for the response rules -- criteria, responses, rules, and matchers. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef RESPONSE_HOST_INTERFACE_H +#define RESPONSE_HOST_INTERFACE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "filesystem.h" +class IUniformRandomStream; +class ICommandLine; + +namespace ResponseRules +{ + // FUNCTIONS YOU MUST IMPLEMENT IN THE HOST EXECUTABLE: + // These are functions that are mentioned in the header, but need their bodies implemented + // in the .dll that links against this lib. + // This is to wrap functions that previously came from the engine interface + // back when the response rules were inside the server.dll . Now that the rules + // are included into a standalone editor, we don't necessarily have an engine around, + // so there needs to be some other implementation. + abstract_class IEngineEmulator + { + public: + /// Given an input text buffer data pointer, parses a single token into the variable token and returns the new + /// reading position + virtual const char *ParseFile( const char *data, char *token, int maxlen ) = 0; + + /// Return a pointer to an IFileSystem we can use to read and process scripts. + virtual IFileSystem *GetFilesystem() = 0; + + /// Return a pointer to an instance of an IUniformRandomStream + virtual IUniformRandomStream *GetRandomStream() = 0 ; + + /// Return a pointer to a tier0 ICommandLine + virtual ICommandLine *GetCommandLine() = 0; + + /// Emulates the server's UTIL_LoadFileForMe + virtual byte *LoadFileForMe( const char *filename, int *pLength ) = 0; + + /// Emulates the server's UTIL_FreeFile + virtual void FreeFile( byte *buffer ) = 0; + + + /// Somewhere in the host executable you should define this symbol and + /// point it at a singleton instance. + static IEngineEmulator *s_pSingleton; + + // this is just a function that returns the pointer above -- just in + // case we need to define it differently. And I get asserts this way. + static IEngineEmulator *Get(); + }; +}; + + +#endif \ No newline at end of file diff --git a/src/public/responserules/response_types.h b/src/public/responserules/response_types.h new file mode 100644 index 000000000..7e60208bb --- /dev/null +++ b/src/public/responserules/response_types.h @@ -0,0 +1,437 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: Core types for the response rules -- criteria, responses, rules, and matchers. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef RESPONSE_TYPES_H +#define RESPONSE_TYPES_H +#ifdef _WIN32 +#pragma once +#endif + +#include "tier1/utlrbtree.h" +#include "tier1/utlsymbol.h" +#include "tier1/interval.h" +#include "mathlib/compressed_vector.h" +#include "datamap.h" +#include "soundflags.h" +#include "tier1/utlsymbol.h" + +namespace ResponseRules +{ + /// Custom symbol table for the response rules. + extern CUtlSymbolTable g_RS; +}; + +#ifdef _MANAGED +// forward declare some editor types just so we can friend them. +namespace ResponseRulesCLI +{ + ref class ResponseQueryResult; +} +#endif + +namespace ResponseRules +{ + using ::DataMapAccess; + //using ::DataMapInit; + class CResponseSystem; + +#pragma pack(push,1) + template + struct response_interval_t + { + T start; + T range; + + interval_t &ToInterval( interval_t &dest ) const { dest.start = start; dest.range = range; return dest; } + void FromInterval( const interval_t &from ) { start = from.start; range = from.range; } + float Random() const { interval_t temp = { start, range }; return RandomInterval( temp ); } + }; + + typedef response_interval_t responseparams_interval_t; +#pragma pack(pop) + +#pragma pack(push,1) + struct AI_ResponseFollowup + { + + + // TODO: make less wasteful of memory, by using a symbol table. + const char *followup_concept; // 12 -- next response + const char *followup_contexts; // 16 + float followup_delay; // 20 + const char *followup_target; // 24 -- to whom is this despatched? + // AIConceptHandle_t hConcept; + const char *followup_entityiotarget; //< if this rule involves firing entity io + const char *followup_entityioinput; //< if this rule involves firing entity io + float followup_entityiodelay; + bool bFired; + + inline bool IsValid( void ) const { return (followup_concept && followup_contexts); } + inline void Invalidate() { followup_concept = NULL; followup_contexts = NULL; } + inline void SetFired( bool fired ) { bFired = fired; } + inline bool HasBeenFired() { return bFired; } + + AI_ResponseFollowup( void ) : followup_concept(NULL), followup_contexts(NULL), followup_delay(0), followup_target(NULL), followup_entityiotarget(NULL), followup_entityioinput(NULL), followup_entityiodelay(0), bFired(false) + {}; + AI_ResponseFollowup( char *_followup_concept, char *_followup_contexts, float _followup_delay, char *_followup_target, + char *_followup_entityiotarget, char *_followup_entityioinput, float _followup_entityiodelay ) : + followup_concept(_followup_concept), followup_contexts(_followup_contexts), followup_delay(_followup_delay), followup_target(_followup_target), + followup_entityiotarget(_followup_entityiotarget), followup_entityioinput(_followup_entityioinput), followup_entityiodelay(_followup_entityiodelay), + bFired(false) + {}; + }; +#pragma pack(pop) + + + enum ResponseType_t + { + RESPONSE_NONE = 0, + RESPONSE_SPEAK, + RESPONSE_SENTENCE, + RESPONSE_SCENE, + RESPONSE_RESPONSE, // A reference to another response by name + RESPONSE_PRINT, + RESPONSE_ENTITYIO, // poke an input on an entity + + NUM_RESPONSES, + }; + + //MAPBASE: + // I wanted to add more options to apply contexts to, +// so I replaced the existing system with a flag-based integer instead of a bunch of booleans. +// +// New ones should be implemented in: +// CResponseSystem::ParseRule() - AI_ResponseSystem.cpp +// AI_Response::Describe() - AI_Criteria.cpp +// CAI_Expresser::SpeakDispatchResponse() - ai_speech.cpp + enum + { + APPLYCONTEXT_SELF = (1 << 0), // Included for contexts that apply to both self and something else + APPLYCONTEXT_WORLD = (1 << 1), // Apply to world + + APPLYCONTEXT_SQUAD = (1 << 2), // Apply to squad + APPLYCONTEXT_ENEMY = (1 << 3), // Apply to enemy + }; + +#pragma pack(push,1) + struct ResponseParams + { + DECLARE_SIMPLE_DATADESC_INSIDE_NAMESPACE(); + + enum + { + RG_DELAYAFTERSPEAK = (1<<0), + RG_SPEAKONCE = (1<<1), + RG_ODDS = (1<<2), + RG_RESPEAKDELAY = (1<<3), + RG_SOUNDLEVEL = (1<<4), + RG_DONT_USE_SCENE = (1<<5), + RG_STOP_ON_NONIDLE = (1<<6), + RG_WEAPONDELAY = (1<<7), + RG_DELAYBEFORESPEAK = (1<<8), + }; + + ResponseParams() + { + flags = 0; + odds = 100; + delay.start = 0; + delay.range = 0; + respeakdelay.start = 0; + respeakdelay.range = 0; + weapondelay.start = 0; + weapondelay.range = 0; + soundlevel = 0; + predelay.start = 0; + predelay.range = 0; + } + responseparams_interval_t delay; //4 + responseparams_interval_t respeakdelay; //8 + responseparams_interval_t weapondelay; //12 + + short odds; //14 + + short flags; //16 + byte soundlevel; //17 + + responseparams_interval_t predelay; //21 + + ALIGN32 AI_ResponseFollowup *m_pFollowup; + + }; +#pragma pack(pop) + + class CriteriaSet + { + public: + typedef CUtlSymbol CritSymbol_t; ///< just to make it clear that some symbols come out of our special static table + public: + CriteriaSet(); + CriteriaSet( const CriteriaSet& src ); + CriteriaSet( const char *criteria, const char *value ) ; // construct initialized with a key/value pair (convenience) + ~CriteriaSet(); + + static CritSymbol_t ComputeCriteriaSymbol( const char *criteria ); + void AppendCriteria( CritSymbol_t criteria, const char *value = "", float weight = 1.0f ); + void AppendCriteria( const char *criteria, const char *value = "", float weight = 1.0f ); + void AppendCriteria( const char *criteria, float value, float weight = 1.0f ); + void RemoveCriteria( const char *criteria ); + + void Describe() const; + + int GetCount() const; + int FindCriterionIndex( CritSymbol_t criteria ) const; + int FindCriterionIndex( const char *name ) const; + inline bool IsValidIndex( int index ) const; + + CritSymbol_t GetNameSymbol( int nIndex ) const; + inline static const char *SymbolToStr( const CritSymbol_t &symbol ); + const char *GetName( int index ) const; + const char *GetValue( int index ) const; + float GetWeight( int index ) const; + + /// Merge another CriteriaSet into this one. + void Merge( const CriteriaSet *otherCriteria ); + void Merge( const char *modifiers ); // add criteria parsed from a text string + + /// add all of the contexts herein onto an entity. all durations are infinite. + void WriteToEntity( CBaseEntity *pEntity ); + + // Accessors to things that need only be done under unusual circumstances. + inline void EnsureCapacity( int num ); + void Reset(); // clear out this criteria (should not be necessary) + + /// When this is true, calls to AppendCriteria on a criteria that already exists + /// will override the existing value. (This is the default behavior). Can be temporarily + /// set false to prevent such overrides. + inline void OverrideOnAppend( bool bOverride ) { m_bOverrideOnAppend = bOverride; } + + // For iteration from beginning to end (also should not be necessary except in + // save/load) + inline int Head() const; + inline int Next( int i ) const; // use with IsValidIndex above + + const static char kAPPLYTOWORLDPREFIX = '$'; + + /// A last minute l4d2 change: deferred contexts prefixed with a '$' + /// character are actually applied to the world. This matches the + /// related hack in CBaseEntity::AppplyContext. + /// This function works IN-PLACE on the "from" parameter. + /// any $-prefixed criteria in pFrom become prefixed by "world", + /// and are also written into pSetOnWorld. + /// *IF* a response matches using the modified criteria, then and only + /// then should you write back the criteria in pSetOnWorld to the world + /// entity, subsequent to the match but BEFORE the dispatch. + /// Returns the number of contexts modified. If it returns 0, then + /// pSetOnWorld is empty. + static int InterceptWorldSetContexts( CriteriaSet * RESTRICT pFrom, + CriteriaSet * RESTRICT pSetOnWorld ); + + private: + void RemoveCriteria( int idx, bool bTestForPrefix ); + + struct CritEntry_t + { + CritEntry_t() : + criterianame( UTL_INVAL_SYMBOL ), + weight( 0.0f ) + { + value[ 0 ] = 0; + } + + CritEntry_t( const CritEntry_t& src ) + { + criterianame = src.criterianame; + value[ 0 ] = 0; + weight = src.weight; + SetValue( src.value ); + } + + CritEntry_t& operator=( const CritEntry_t& src ) + { + if ( this == &src ) + return *this; + + criterianame = src.criterianame; + weight = src.weight; + SetValue( src.value ); + + return *this; + } + + static bool LessFunc( const CritEntry_t& lhs, const CritEntry_t& rhs ) + { + return lhs.criterianame < rhs.criterianame; + } + + void SetValue( char const *str ) + { + if ( !str ) + { + value[ 0 ] = 0; + } + else + { + Q_strncpy( value, str, sizeof( value ) ); + } + } + + CritSymbol_t criterianame; + char value[ 64 ]; + float weight; + }; + + static CUtlSymbolTable sm_CriteriaSymbols; + typedef CUtlRBTree< CritEntry_t, short > Dict_t; + Dict_t m_Lookup; + int m_nNumPrefixedContexts; // number of contexts prefixed with kAPPLYTOWORLDPREFIX + bool m_bOverrideOnAppend; + }; + + inline void CriteriaSet::EnsureCapacity( int num ) + { + m_Lookup.EnsureCapacity(num); + } + + //----------------------------------------------------------------------------- + // Purpose: Generic container for a response to a match to a criteria set + // This is what searching for a response returns + //----------------------------------------------------------------------------- + + class CRR_Response + { + public: + DECLARE_SIMPLE_DATADESC_INSIDE_NAMESPACE(); + + CRR_Response(); + CRR_Response( const CRR_Response &from ); + CRR_Response &operator=( const CRR_Response &from ); + ~CRR_Response(); + private: + void operator delete(void* p); // please do not new or delete CRR_Responses. + public: + + // void Release(); // we no longer encourage new and delete on these things + + void GetName( char *buf, size_t buflen ) const; + void GetResponse( char *buf, size_t buflen ) const; + const char* GetNamePtr() const; + const char* GetResponsePtr() const; + const ResponseParams *GetParams() const { return &m_Params; } + ResponseType_t GetType() const { return (ResponseType_t)m_Type; } + soundlevel_t GetSoundLevel() const; + float GetRespeakDelay() const; + float GetWeaponDelay() const; + bool GetSpeakOnce() const; + bool ShouldntUseScene( ) const; + bool ShouldBreakOnNonIdle( void ) const; + int GetOdds() const; + float GetDelay() const; + float GetPreDelay() const; + + inline bool IsEmpty() const; // true iff my response name is empty + void Invalidate() ; // wipe out my contents, mark me invalid + + // Get/set the contexts we apply to character and world after execution + void SetContext( const char *context ); + const char * GetContext( void ) const { return m_szContext; } + + // Get/set the score I matched with (under certain circumstances) + inline float GetMatchScore( void ) { return m_fMatchScore; } + inline void SetMatchScore( float f ) { m_fMatchScore = f; } + + int GetContextFlags() { return m_iContextFlags; } + bool IsApplyContextToWorld() const { return (m_iContextFlags & APPLYCONTEXT_WORLD) != 0; } + + void Describe( const CriteriaSet *pDebugCriteria = NULL ); + + void Init( ResponseType_t type, + const char *responseName, + const ResponseParams& responseparams, + const char *matchingRule, + const char *applyContext, + int iContextFlags ); + + static const char *DescribeResponse( ResponseType_t type ); + + enum + { + MAX_RESPONSE_NAME = 64, + MAX_RULE_NAME = 64 + }; + + + private: + byte m_Type; + char m_szResponseName[ MAX_RESPONSE_NAME ]; + char m_szMatchingRule[ MAX_RULE_NAME ]; + + ResponseParams m_Params; + float m_fMatchScore; // when instantiated dynamically in SpeakFindResponse, the score of the rule that matched it. + + char * m_szContext; // context data we apply to character after running + int m_iContextFlags; + +#ifdef _MANAGED + friend ref class ResponseRulesCLI::ResponseQueryResult; +#endif + }; + + + + abstract_class IResponseFilter + { + public: + virtual bool IsValidResponse( ResponseType_t type, const char *pszValue ) = 0; + }; + + abstract_class IResponseSystem + { + public: + virtual ~IResponseSystem() {} + + virtual bool FindBestResponse( const CriteriaSet& set, CRR_Response& response, IResponseFilter *pFilter = NULL ) = 0; + virtual void GetAllResponses( CUtlVector *pResponses ) = 0; + virtual void PrecacheResponses( bool bEnable ) = 0; + }; + + + + // INLINE FUNCTIONS + + // Used as a failsafe in finding responses. + bool CRR_Response::IsEmpty() const + { + return m_szResponseName[0] == 0; + } + + inline bool CriteriaSet::IsValidIndex( int index ) const + { + return ( index >= 0 && index < ((int)(m_Lookup.Count())) ); + } + + inline int CriteriaSet::Head() const + { + return m_Lookup.FirstInorder(); + } + + inline int CriteriaSet::Next( int i ) const + { + return m_Lookup.NextInorder(i); + } + + inline const char *CriteriaSet::SymbolToStr( const CritSymbol_t &symbol ) + { + return sm_CriteriaSymbols.String(symbol); + } + +} + +#include "rr_speechconcept.h" +#include "response_host_interface.h" + +#endif diff --git a/src/public/responserules/rr_speechconcept.h b/src/public/responserules/rr_speechconcept.h new file mode 100644 index 000000000..65b1bb6e6 --- /dev/null +++ b/src/public/responserules/rr_speechconcept.h @@ -0,0 +1,57 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: Class data for an AI Concept, an atom of response-driven dialog. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef RR_SPEECHCONCEPT_H +#define RR_SPEECHCONCEPT_H + +#if defined( _WIN32 ) +#pragma once +#endif + +#include "utlsymbol.h" + +#define RR_CONCEPTS_ARE_STRINGS 0 + + +typedef CUtlSymbolTable CRR_ConceptSymbolTable; + +namespace ResponseRules +{ +class CRR_Concept +{ +public: // local typedefs + typedef CUtlSymbol tGenericId; // an int-like type that can be used to refer to all concepts of this type + tGenericId m_iConcept; + +public: + CRR_Concept() {}; + // construct concept from a string. + CRR_Concept(const char *fromString); + + // Return as a string + const char *GetStringConcept() const; + static const char *GetStringForGenericId(tGenericId genericId); + + operator tGenericId() const { return m_iConcept; } + operator const char *() const { return GetStringConcept(); } + inline bool operator==(const CRR_Concept &other) // default is compare by concept ids + { + return m_iConcept == other.m_iConcept; + } + bool operator==(const char *pszConcept); + +protected: + +private: + // dupe a concept + // CRR_Concept& operator=(CRR_Concept &other); + CRR_Concept& operator=(const char *fromString); +}; +}; + + +#endif diff --git a/src/public/tier0/basetypes.h b/src/public/tier0/basetypes.h index d4208dbcd..7b9df1d4a 100644 --- a/src/public/tier0/basetypes.h +++ b/src/public/tier0/basetypes.h @@ -131,6 +131,70 @@ constexpr T Max( T const &val1, T const &val2 ) #define TRUE (!FALSE) #endif +//----------------------------------------------------------------------------- +// fsel +//----------------------------------------------------------------------------- +#ifndef _X360 + +#define fsel(c,x,y) ( (c) >= 0 ? (x) : (y) ) + +// integer conditional move +// if a >= 0, return x, else y +#define isel(a,x,y) ( ((a) >= 0) ? (x) : (y) ) + +// if x = y, return a, else b +#define ieqsel(x,y,a,b) (( (x) == (y) ) ? (a) : (b)) + +// if the nth bit of a is set (counting with 0 = LSB), +// return x, else y +// this is fast if nbit is a compile-time immediate +#define ibitsel(a, nbit, x, y) ( ( ((a) & (1 << (nbit))) != 0 ) ? (x) : (y) ) + +#else + +// __fsel(double fComparand, double fValGE, double fLT) == fComparand >= 0 ? fValGE : fLT +// this is much faster than if ( aFloat > 0 ) { x = .. } +// the XDK defines two intrinsics, one for floats and one for doubles -- it's the same +// opcode, but the __fself version tells the compiler not to do a wasteful unnecessary +// rounding op after each sel. +// #define fsel __fsel +FORCEINLINE double fsel(double fComparand, double fValGE, double fLT) { return __fsel( fComparand, fValGE, fLT ); } +FORCEINLINE float fsel(float fComparand, float fValGE, float fLT) { return __fself( fComparand, fValGE, fLT ); } + +// if a >= 0, return x, else y +FORCEINLINE int isel( int a, int x, int y ) +{ + int mask = a >> 31; // arithmetic shift right, splat out the sign bit + return x + ((y - x) & mask); +}; + +// if a >= 0, return x, else y +FORCEINLINE unsigned isel( int a, unsigned x, unsigned y ) +{ + int mask = a >> 31; // arithmetic shift right, splat out the sign bit + return x + ((y - x) & mask); +}; + +// ( x == y ) ? a : b +FORCEINLINE unsigned ieqsel( unsigned x, unsigned y, unsigned a, unsigned b ) +{ + unsigned mask = (x == y) ? 0 : -1; + return a + ((b - a) & mask); +}; + +// ( x == y ) ? a : b +FORCEINLINE int ieqsel( int x, int y, int a, int b ) +{ + int mask = (x == y) ? 0 : -1; + return a + ((b - a) & mask); +}; + +// if the nth bit of a is set (counting with 0 = LSB), +// return x, else y +// this is fast if nbit is a compile-time immediate +#define ibitsel(a, nbit, x, y) ( (x) + (((y) - (x)) & (((a) & (1 << (nbit))) ? 0 : -1)) ) + +#endif #ifndef DONT_DEFINE_BOOL // Needed for Cocoa stuff to compile. typedef int BOOL; diff --git a/src/public/tier0/platform.h b/src/public/tier0/platform.h index 500edcb62..5039653f4 100644 --- a/src/public/tier0/platform.h +++ b/src/public/tier0/platform.h @@ -800,29 +800,6 @@ typedef uint32 HMODULE; typedef void *HANDLE; #endif -//----------------------------------------------------------------------------- -// fsel -//----------------------------------------------------------------------------- -#ifndef _X360 - -static FORCEINLINE float fsel(float fComparand, float fValGE, float fLT) -{ - return fComparand >= 0 ? fValGE : fLT; -} -static FORCEINLINE double fsel(double fComparand, double fValGE, double fLT) -{ - return fComparand >= 0 ? fValGE : fLT; -} - -#else - -// __fsel(double fComparand, double fValGE, double fLT) == fComparand >= 0 ? fValGE : fLT -// this is much faster than if ( aFloat > 0 ) { x = .. } -#define fsel __fsel - -#endif - - //----------------------------------------------------------------------------- // FP exception handling //----------------------------------------------------------------------------- diff --git a/src/public/tier1/generichash.h b/src/public/tier1/generichash.h index 7e7f002b3..6c79bb58d 100644 --- a/src/public/tier1/generichash.h +++ b/src/public/tier1/generichash.h @@ -41,6 +41,12 @@ inline uint32 HashStringCaseless( const char *pszKey, size_t len ) return MurmurHash3_32( pszKey, len, 1047 /*anything will do for a seed*/, true ); } +inline uint32 HashStringCaselessConventional(const char* pszKey) +{ + size_t len = strlen(pszKey); + HashStringCaseless(pszKey, len); +} + #if !defined(_MINIMUM_BUILD_) inline uint32 HashString( const char *pszKey ) { diff --git a/src/game/shared/interval.h b/src/public/tier1/interval.h similarity index 100% rename from src/game/shared/interval.h rename to src/public/tier1/interval.h diff --git a/src/responserules/runtime/criteriaset.cpp b/src/responserules/runtime/criteriaset.cpp new file mode 100644 index 000000000..3dc5cb200 --- /dev/null +++ b/src/responserules/runtime/criteriaset.cpp @@ -0,0 +1,477 @@ +//===== Copyright © 1996-2005, Valve Corporation, All rights reserved. ======// +// +// Purpose: +// +// $NoKeywords: $ +// +//===========================================================================// +#include "rrbase.h" + +#include "utlmap.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include + +using namespace ResponseRules; + +//----------------------------------------------------------------------------- +// Case-insensitive criteria symbol table +//----------------------------------------------------------------------------- +CUtlSymbolTable CriteriaSet::sm_CriteriaSymbols( 1024, 1024, true ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *raw - +// *key - +// keylen - +// *value - +// valuelen - +// *duration - +// Output : static bool +//----------------------------------------------------------------------------- +const char *SplitContext( const char *raw, char *key, int keylen, char *value, int valuelen, float *duration, const char *entireContext ) +{ + char *colon1 = Q_strstr( raw, ":" ); + if ( !colon1 ) + { + DevMsg( "SplitContext: warning, ignoring context '%s', missing colon separator!\n", raw ); + *key = *value = 0; + return NULL; + } + + int len = colon1 - raw; + Q_strncpy( key, raw, MIN( len + 1, keylen ) ); + key[ MIN( len, keylen - 1 ) ] = 0; + + bool last = false; + char *end = Q_strstr( colon1 + 1, "," ); + if ( !end ) + { + int remaining = Q_strlen( colon1 + 1 ); + end = colon1 + 1 + remaining; + last = true; + } + + char *colon2 = Q_strstr( colon1 + 1, ":" ); + if ( colon2 && ( colon2 < end ) ) + { + if ( duration ) + *duration = atof( colon2 + 1 ); + + char durationStartChar = *(colon2 + 1); + if ( durationStartChar < '0' || durationStartChar > '9' ) + { + DevMsg( "SplitContext: warning, ignoring context '%s', missing comma separator! Entire context was '%s'.\n", raw, entireContext ); + *key = *value = 0; + return NULL; + } + + len = MIN( colon2 - ( colon1 + 1 ), valuelen - 1 ); + Q_strncpy( value, colon1 + 1, len + 1 ); + value[ len ] = 0; + } + else + { + if ( duration ) + *duration = 0.0; + + len = MIN( end - ( colon1 + 1 ), valuelen - 1 ); + Q_strncpy( value, colon1 + 1, len + 1 ); + value[ len ] = 0; + } + + return last ? NULL : end + 1; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CriteriaSet::CriteriaSet() : m_Lookup( 0, 0, CritEntry_t::LessFunc ), m_bOverrideOnAppend(true), + m_nNumPrefixedContexts(0) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CriteriaSet::CriteriaSet( const CriteriaSet& src ) : m_Lookup( 0, 0, CritEntry_t::LessFunc ), m_nNumPrefixedContexts(src.m_nNumPrefixedContexts) +{ + m_Lookup.EnsureCapacity( src.m_Lookup.Count() ); + for ( short i = src.m_Lookup.FirstInorder(); + i != src.m_Lookup.InvalidIndex(); + i = src.m_Lookup.NextInorder( i ) ) + { + m_Lookup.Insert( src.m_Lookup[ i ] ); + } +} + +CriteriaSet::CriteriaSet( const char *criteria, const char *value ) : m_Lookup( 0, 0, CritEntry_t::LessFunc ), m_bOverrideOnAppend(true) +{ + AppendCriteria(criteria,value); +} + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CriteriaSet::~CriteriaSet() +{ +} + +//----------------------------------------------------------------------------- +// Computes a symbol for the criteria +//----------------------------------------------------------------------------- +CriteriaSet::CritSymbol_t CriteriaSet::ComputeCriteriaSymbol( const char *criteria ) +{ + return sm_CriteriaSymbols.AddString( criteria ); +} + + +//----------------------------------------------------------------------------- +// Computes a symbol for the criteria +//----------------------------------------------------------------------------- +void CriteriaSet::AppendCriteria( CriteriaSet::CritSymbol_t criteria, const char *value, float weight ) +{ + int idx = FindCriterionIndex( criteria ); + if ( idx == -1 ) + { + CritEntry_t entry; + entry.criterianame = criteria; + MEM_ALLOC_CREDIT(); + idx = m_Lookup.Insert( entry ); + if ( sm_CriteriaSymbols.String(criteria)[0] == kAPPLYTOWORLDPREFIX ) + { + m_nNumPrefixedContexts += 1; + } + } + else // criteria already existed + { + // bail out if override existing criteria is not allowed + if ( !m_bOverrideOnAppend ) + return; + } + + CritEntry_t *entry = &m_Lookup[ idx ]; + entry->SetValue( value ); + entry->weight = weight; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *criteria - +// "" - +// 1.0f - +//----------------------------------------------------------------------------- +void CriteriaSet::AppendCriteria( const char *pCriteriaName, const char *value /*= ""*/, float weight /*= 1.0f*/ ) +{ + CUtlSymbol criteria = ComputeCriteriaSymbol( pCriteriaName ); + AppendCriteria( criteria, value, weight ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *criteria - +// "" - +// 1.0f - +//----------------------------------------------------------------------------- +void CriteriaSet::AppendCriteria( const char *criteria, float value, float weight /*= 1.0f*/ ) +{ + char buf[32]; + V_snprintf( buf, 32, "%f", value ); + AppendCriteria( criteria, buf, weight ); +} + + +//----------------------------------------------------------------------------- +// Removes criteria in a set +//----------------------------------------------------------------------------- +void CriteriaSet::RemoveCriteria( const char *criteria ) +{ + const int idx = FindCriterionIndex( criteria ); + if ( idx == -1 ) + return; + + if ( criteria[0] == kAPPLYTOWORLDPREFIX ) + { + Assert( m_nNumPrefixedContexts > 0 ); + m_nNumPrefixedContexts = isel( m_nNumPrefixedContexts - 1, m_nNumPrefixedContexts - 1, 0 ); + } + RemoveCriteria( idx, false ); +} + +// bTestForIndex tells us whether the calling function has already checked for a +// $ prefix and decremented m_nNumPrefixedContexts appropriately (false), +// or if this function should do that (true). +void CriteriaSet::RemoveCriteria( int idx, bool bTestForPrefix ) +{ + Assert( m_Lookup.IsValidIndex(idx) ); + if ( bTestForPrefix ) + { + if ( sm_CriteriaSymbols.String( m_Lookup[idx].criterianame )[0] == kAPPLYTOWORLDPREFIX ) + { + Assert( m_nNumPrefixedContexts > 0 ); + m_nNumPrefixedContexts = isel( m_nNumPrefixedContexts - 1, m_nNumPrefixedContexts - 1, 0 ); + } + } + m_Lookup.RemoveAt( idx ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : int +//----------------------------------------------------------------------------- +int CriteriaSet::GetCount() const +{ + return m_Lookup.Count(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +// Output : int +//----------------------------------------------------------------------------- +int CriteriaSet::FindCriterionIndex( CritSymbol_t criteria ) const +{ + CritEntry_t search; + search.criterianame = criteria; + int idx = m_Lookup.Find( search ); + return ( idx == m_Lookup.InvalidIndex() ) ? -1 : idx; +} + +int CriteriaSet::FindCriterionIndex( const char *name ) const +{ + CUtlSymbol criteria = ComputeCriteriaSymbol( name ); + return FindCriterionIndex( criteria ); +} + + +//----------------------------------------------------------------------------- +// Returns the name symbol +//----------------------------------------------------------------------------- +CriteriaSet::CritSymbol_t CriteriaSet::GetNameSymbol( int nIndex ) const +{ + if ( nIndex < 0 || nIndex >= (int)m_Lookup.Count() ) + return UTL_INVAL_SYMBOL; + + const CritEntry_t *entry = &m_Lookup[ nIndex ]; + return entry->criterianame; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : index - +// Output : char const +//----------------------------------------------------------------------------- +const char *CriteriaSet::GetName( int index ) const +{ + if ( index < 0 || index >= (int)m_Lookup.Count() ) + return ""; + else + { + const char *pCriteriaName = sm_CriteriaSymbols.String( m_Lookup[ index ].criterianame ); + return pCriteriaName ? pCriteriaName : ""; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : index - +// Output : char const +//----------------------------------------------------------------------------- +const char *CriteriaSet::GetValue( int index ) const +{ + if ( index < 0 || index >= (int)m_Lookup.Count() ) + return ""; + + const CritEntry_t *entry = &m_Lookup[ index ]; + return entry->value ? entry->value : ""; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : index - +// Output : float +//----------------------------------------------------------------------------- +float CriteriaSet::GetWeight( int index ) const +{ + if ( index < 0 || index >= (int)m_Lookup.Count() ) + return 1.0f; + + const CritEntry_t *entry = &m_Lookup[ index ]; + return entry->weight; +} + + +//----------------------------------------------------------------------------- +// Purpose: Merge another criteria set into this one. +//----------------------------------------------------------------------------- +void CriteriaSet::Merge( const CriteriaSet * RESTRICT otherCriteria ) +{ + Assert(otherCriteria); + if (!otherCriteria) + return; + + // for now, just duplicate everything. + int count = otherCriteria->GetCount(); + EnsureCapacity( count + GetCount() ); + for ( int i = 0 ; i < count ; ++i ) + { + AppendCriteria( otherCriteria->GetNameSymbol(i), otherCriteria->GetValue(i), otherCriteria->GetWeight(i) ); + } +} + +void CriteriaSet::Merge( const char *modifiers ) // add criteria parsed from a text string +{ + // Always include any optional modifiers + if ( modifiers == NULL ) + return; + + char copy_modifiers[ 255 ]; + const char *pCopy; + char key[ 128 ] = { 0 }; + char value[ 128 ] = { 0 }; + + Q_strncpy( copy_modifiers, modifiers, sizeof( copy_modifiers ) ); + pCopy = copy_modifiers; + + while( pCopy ) + { + pCopy = SplitContext( pCopy, key, sizeof( key ), value, sizeof( value ), NULL, modifiers ); + + if( *key && *value ) + { + AppendCriteria( key, value, 1 ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CriteriaSet::Describe() const +{ + // build an alphabetized representation of the set for printing + typedef CUtlMap tMap; + tMap m_TempMap( 0, m_Lookup.Count(), CaselessStringLessThan ); + + for ( short i = m_Lookup.FirstInorder(); i != m_Lookup.InvalidIndex(); i = m_Lookup.NextInorder( i ) ) + { + const CritEntry_t *entry = &m_Lookup[ i ]; + + m_TempMap.Insert( sm_CriteriaSymbols.String( entry->criterianame ), entry ); + } + + for ( tMap::IndexType_t i = m_TempMap.FirstInorder(); i != m_TempMap.InvalidIndex(); i = m_TempMap.NextInorder( i ) ) + { + // const CritEntry_t *entry = &m_TempMap[ i ]; + // const char *pCriteriaName = sm_CriteriaSymbols.String( entry->criterianame ); + + const char *name = m_TempMap.Key( i ); + const CritEntry_t *entry = m_TempMap.Element( i ); + if ( entry->weight != 1.0f ) + { + DevMsg( " %20s = '%s' (weight %f)\n", name, entry->value ? entry->value : "", entry->weight ); + } + else + { + DevMsg( " %20s = '%s'\n", name, entry->value ? entry->value : "" ); + } + } + + /* + for ( short i = m_Lookup.FirstInorder(); i != m_Lookup.InvalidIndex(); i = m_Lookup.NextInorder( i ) ) + { + const CritEntry_t *entry = &m_Lookup[ i ]; + + const char *pCriteriaName = sm_CriteriaSymbols.String( entry->criterianame ); + if ( entry->weight != 1.0f ) + { + DevMsg( " %20s = '%s' (weight %f)\n", pCriteriaName, entry->value ? entry->value : "", entry->weight ); + } + else + { + DevMsg( " %20s = '%s'\n", pCriteriaName, entry->value ? entry->value : "" ); + } + } + */ +} + + +void CriteriaSet::Reset() +{ + m_Lookup.Purge(); +} + +void CriteriaSet::WriteToEntity( CBaseEntity *pEntity ) +{ +#if 0 + if ( GetCount() < 1 ) + return; + + for ( int i = Head() ; IsValidIndex(i); i = Next(i) ) + { + pEntity->AddContext( GetName(i), GetValue(i), 0 ); + } +#else + AssertMsg( false, "CriteriaSet::WriteToEntity has not been ported from l4d2.\n" ); +#endif +} + +int CriteriaSet::InterceptWorldSetContexts( CriteriaSet * RESTRICT pFrom, CriteriaSet * RESTRICT pSetOnWorld ) +{ + // Assert( pFrom ); Assert( pTo ); Assert( pSetOnWorld ); + Assert( pSetOnWorld != pFrom ); + Assert( pSetOnWorld->GetCount() == 0 ); + + if ( pFrom->m_nNumPrefixedContexts == 0 ) + { + // nothing needs to be done to it. + return 0; + } + +#ifdef DEBUG + // save this off for later error checking. + const int nPrefixedContexts = pFrom->m_nNumPrefixedContexts; +#endif + + // make enough space for the expected output quantity. + pSetOnWorld->EnsureCapacity( pFrom->m_nNumPrefixedContexts ); + + // initialize a buffer with the "world" prefix (so we can use strncpy instead of snprintf and be much faster) + char buf[80] = { 'w', 'o', 'r', 'l', 'd', '\0' }; + const unsigned int PREFIXLEN = 5; // strlen("world") + + // create a second tree that has the appropriately renamed criteria, + // then swap it into pFrom + CriteriaSet rewrite; + rewrite.EnsureCapacity( pFrom->GetCount() + 1 ); + + for ( int i = pFrom->Head(); pFrom->IsValidIndex(i); i = pFrom->Next(i) ) + { + const char *pszName = pFrom->GetName( i ); + if ( pszName[0] == CriteriaSet::kAPPLYTOWORLDPREFIX ) + { // redirect to the world contexts + V_strncpy( buf+PREFIXLEN, pszName+1, sizeof(buf) - PREFIXLEN ); + rewrite.AppendCriteria( buf, pFrom->GetValue(i), pFrom->GetWeight(i) ); + pSetOnWorld->AppendCriteria( pszName+1, pFrom->GetValue(i), pFrom->GetWeight(i) ); + buf[PREFIXLEN] = 0; + } + else + { // does not need to be fiddled; do not write back to world + rewrite.AppendCriteria( pFrom->GetNameSymbol(i), pFrom->GetValue(i), pFrom->GetWeight(i) ); + } + } + AssertMsg2( pSetOnWorld->GetCount() == nPrefixedContexts, "Count of $ persistent RR contexts is inconsistent (%d vs %d)! Call Elan.", + pSetOnWorld->GetCount(), nPrefixedContexts ); + + pFrom->m_nNumPrefixedContexts = 0; + pFrom->m_Lookup.Swap(rewrite.m_Lookup); + return pSetOnWorld->GetCount(); +} diff --git a/src/responserules/runtime/response_rules.vpc b/src/responserules/runtime/response_rules.vpc new file mode 100644 index 000000000..164f48564 --- /dev/null +++ b/src/responserules/runtime/response_rules.vpc @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +// response_rules.VPC +// +// Project Script +//----------------------------------------------------------------------------- + +$macro SRCDIR "..\.." +$include "$SRCDIR\vpc_scripts\source_lib_base.vpc" + +$Configuration +{ + $Compiler + { + $AdditionalIncludeDirectories "$BASE;..\public\responserules" + $PreprocessorDefinitions "$BASE;RR_RUNTIME" + } +} + +$Project "responserules_runtime" +{ + $Folder "Source Files" + { + $File "criteriaset.cpp" + $File "response_system.cpp" + $File "response_system.h" + $File "response_types.cpp" + $File "response_types_internal.cpp" + $File "response_types_internal.h" + $File "rr_convars.cpp" + $File "rr_response.cpp" + $File "rr_speechconcept.cpp" + $File "rrrlib.cpp" + } + + $Folder "Public Header Files" + { + $File "$SRCDIR\public\responserules\response_host_interface.h" + $File "$SRCDIR\public\responserules\response_types.h" + $File "$SRCDIR\public\responserules\rr_speechconcept.h" + } +} \ No newline at end of file diff --git a/src/responserules/runtime/response_system.cpp b/src/responserules/runtime/response_system.cpp new file mode 100644 index 000000000..b98110dae --- /dev/null +++ b/src/responserules/runtime/response_system.cpp @@ -0,0 +1,2749 @@ +//========= Copyright © 1996-2010, Valve Corporation, All rights reserved. ============// +// +// Purpose: Core types for the response rules -- criteria, responses, rules, and matchers. +// +// $NoKeywords: $ +//=============================================================================// + +#include "rrbase.h" +#include "vstdlib/random.h" +#include "utlbuffer.h" +#include "tier1/interval.h" +#include "convar.h" +#include "fmtstr.h" +#include "generichash.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// #pragma optimize( "", off ) + +using namespace ResponseRules; +static void CC_RR_Debug_ResponseConcept_Exclude( const CCommand &args ); +static ConCommand rr_debug_responseconcept_exclude( "rr_debugresponseconcept_exclude", CC_RR_Debug_ResponseConcept_Exclude, "Set a list of concepts to exclude from rr_debugresponseconcept. Separate multiple concepts with spaces. Call with no arguments to see current list. Call 'rr_debug_responseconcept_exclude !' to reset."); +static void CC_RR_DumpHashInfo( const CCommand &args ); + +namespace ResponseRules +{ + // ick + // Wrap string lookup with a hash on the string so that all of the repetitive playerxxx type strings get bucketed out better + #define STRING_BUCKETS_COUNT 64 // Must be power of two + #define STRING_BUCKETS_MASK ( STRING_BUCKETS_COUNT - 1 ) + + CUtlRBTree g_ResponseStrings[ STRING_BUCKETS_COUNT ]; + class CResponseStringBuckets + { + public: + CResponseStringBuckets() + { + for ( int i = 0; i < ARRAYSIZE( g_ResponseStrings ); ++i ) + { + g_ResponseStrings[ i ].SetLessFunc( &StringLessThan ); + } + } + } g_ReponseStringBucketInitializer; + + const char *ResponseCopyString( const char *in ) + { + if ( !in ) + return NULL; + if ( !*in ) + return ""; + + int bucket = ( RR_HASH( in ) & STRING_BUCKETS_MASK ); + + CUtlRBTree &list = g_ResponseStrings[ bucket ]; + + int i = list.Find( in ); + if ( i != list.InvalidIndex() ) +{ + return list[i]; + } + + int len = Q_strlen( in ); + char *out = new char[ len + 1 ]; + Q_memcpy( out, in, len ); + out[ len ] = 0; + list.Insert( out ); + return out; + } +} + +IEngineEmulator *IEngineEmulator::Get() +{ + AssertMsg( IEngineEmulator::s_pSingleton, "Response rules fail: no IEngineEmulator" ); + return IEngineEmulator::s_pSingleton; +} + + +//----------------------------------------------------------------------------- +// Convars +//----------------------------------------------------------------------------- + + +ConVar rr_debugresponses( "rr_debugresponses", "0", FCVAR_NONE, "Show verbose matching output (1 for simple, 2 for rule scoring, 3 for noisy). If set to 4, it will only show response success/failure for npc_selected NPCs." ); +ConVar rr_debugrule( "rr_debugrule", "", FCVAR_NONE, "If set to the name of the rule, that rule's score will be shown whenever a concept is passed into the response rules system."); +ConVar rr_dumpresponses( "rr_dumpresponses", "0", FCVAR_NONE, "Dump all response_rules.txt and rules (requires restart)" ); +ConVar rr_debugresponseconcept( "rr_debugresponseconcept", "", FCVAR_NONE, "If set, rr_debugresponses will print only responses testing for the specified concept" ); +#define RR_DEBUGRESPONSES_SPECIALCASE 4 + + + +//----------------------------------------------------------------------------- +// Copied from SoundParametersInternal.cpp +//----------------------------------------------------------------------------- + +#define SNDLVL_PREFIX "SNDLVL_" + +struct SoundLevelLookup +{ + soundlevel_t level; + char const *name; +}; + +// NOTE: Needs to reflect the soundlevel_t enum defined in soundflags.h +static SoundLevelLookup g_pSoundLevels[] = +{ + { SNDLVL_NONE, "SNDLVL_NONE" }, + { SNDLVL_20dB, "SNDLVL_20dB" }, + { SNDLVL_25dB, "SNDLVL_25dB" }, + { SNDLVL_30dB, "SNDLVL_30dB" }, + { SNDLVL_35dB, "SNDLVL_35dB" }, + { SNDLVL_40dB, "SNDLVL_40dB" }, + { SNDLVL_45dB, "SNDLVL_45dB" }, + { SNDLVL_50dB, "SNDLVL_50dB" }, + { SNDLVL_55dB, "SNDLVL_55dB" }, + { SNDLVL_IDLE, "SNDLVL_IDLE" }, + { SNDLVL_TALKING, "SNDLVL_TALKING" }, + { SNDLVL_60dB, "SNDLVL_60dB" }, + { SNDLVL_65dB, "SNDLVL_65dB" }, + { SNDLVL_STATIC, "SNDLVL_STATIC" }, + { SNDLVL_70dB, "SNDLVL_70dB" }, + { SNDLVL_NORM, "SNDLVL_NORM" }, + { SNDLVL_75dB, "SNDLVL_75dB" }, + { SNDLVL_80dB, "SNDLVL_80dB" }, + { SNDLVL_85dB, "SNDLVL_85dB" }, + { SNDLVL_90dB, "SNDLVL_90dB" }, + { SNDLVL_95dB, "SNDLVL_95dB" }, + { SNDLVL_100dB, "SNDLVL_100dB" }, + { SNDLVL_105dB, "SNDLVL_105dB" }, + { SNDLVL_110dB, "SNDLVL_110dB" }, + { SNDLVL_120dB, "SNDLVL_120dB" }, + { SNDLVL_130dB, "SNDLVL_130dB" }, + { SNDLVL_GUNFIRE, "SNDLVL_GUNFIRE" }, + { SNDLVL_140dB, "SNDLVL_140dB" }, + { SNDLVL_150dB, "SNDLVL_150dB" }, + { SNDLVL_180dB, "SNDLVL_180dB" }, +}; + +static soundlevel_t TextToSoundLevel( const char *key ) +{ + if ( !key ) + { + Assert( 0 ); + return SNDLVL_NORM; + } + + int c = ARRAYSIZE( g_pSoundLevels ); + + int i; + + for ( i = 0; i < c; i++ ) + { + SoundLevelLookup *entry = &g_pSoundLevels[ i ]; + if ( !Q_strcasecmp( key, entry->name ) ) + return entry->level; + } + + if ( !Q_strnicmp( key, SNDLVL_PREFIX, Q_strlen( SNDLVL_PREFIX ) ) ) + { + char const *val = key + Q_strlen( SNDLVL_PREFIX ); + int sndlvl = atoi( val ); + if ( sndlvl > 0 && sndlvl <= 180 ) + { + return ( soundlevel_t )sndlvl; + } + } + + DevMsg( "CSoundEmitterSystem: Unknown sound level %s\n", key ); + + return SNDLVL_NORM; +} + +CResponseSystem::ExcludeList_t CResponseSystem::m_DebugExcludeList( 4, 0 ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CResponseSystem::CResponseSystem() : + m_RootCommandHashes( 0, 0, DefLessFunc( unsigned int ) ), + m_FileDispatch( 0, 0, DefLessFunc( unsigned int ) ), + m_RuleDispatch( 0, 0, DefLessFunc( unsigned int ) ), + m_ResponseDispatch( 0, 0, DefLessFunc( unsigned int ) ), + m_ResponseGroupDispatch( 0, 0, DefLessFunc( unsigned int ) ) +{ + token[0] = 0; + m_bUnget = false; + m_bCustomManagable = false; + + BuildDispatchTables(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CResponseSystem::~CResponseSystem() +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : char const +//----------------------------------------------------------------------------- +void CResponseSystem::GetCurrentScript( char *buf, size_t buflen ) +{ + Assert( buf ); + buf[ 0 ] = 0; + if ( m_ScriptStack.Count() <= 0 ) + return; + + if ( IEngineEmulator::Get()->GetFilesystem()->String( m_ScriptStack[ 0 ].name, buf, buflen ) ) + { + return; + } + buf[ 0 ] = 0; +} + +void CResponseSystem::PushScript( const char *scriptfile, unsigned char *buffer ) +{ + ScriptEntry e; + e.name = IEngineEmulator::Get()->GetFilesystem()->FindOrAddFileName( scriptfile ); + e.buffer = buffer; + e.currenttoken = (char *)e.buffer; + e.tokencount = 0; + m_ScriptStack.AddToHead( e ); +} + +void CResponseSystem::PopScript(void) +{ + Assert( m_ScriptStack.Count() >= 1 ); + if ( m_ScriptStack.Count() <= 0 ) + return; + + m_ScriptStack.Remove( 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CResponseSystem::Clear() +{ + m_Responses.RemoveAll(); + m_Criteria.RemoveAll(); + m_RulePartitions.RemoveAll(); + m_Enumerations.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +// found - +// Output : float +//----------------------------------------------------------------------------- +float CResponseSystem::LookupEnumeration( const char *name, bool& found ) +{ + int idx = m_Enumerations.Find( name ); + if ( idx == m_Enumerations.InvalidIndex() ) + { + found = false; + return 0.0f; + } + + + found = true; + return m_Enumerations[ idx ].value; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : matcher - +//----------------------------------------------------------------------------- +void CResponseSystem::ResolveToken( Matcher& matcher, char *token, size_t bufsize, char const *rawtoken ) +{ + if ( rawtoken[0] != '[' ) + { + Q_strncpy( token, rawtoken, bufsize ); + return; + } + + // Now lookup enumeration + bool found = false; + float f = LookupEnumeration( rawtoken, found ); + if ( !found ) + { + Q_strncpy( token, rawtoken, bufsize ); + ResponseWarning( "No such enumeration '%s'\n", token ); + return; + } + + Q_snprintf( token, bufsize, "%f", f ); +} + + +static bool AppearsToBeANumber( char const *token ) +{ + if ( atof( token ) != 0.0f ) + return true; + + char const *p = token; + while ( *p ) + { + if ( *p != '0' ) + return false; + + p++; + } + + return true; +} + +void CResponseSystem::ComputeMatcher( Criteria *c, Matcher& matcher ) +{ + const char *s = c->value; + if ( !s ) + { + matcher.valid = false; + return; + } + + const char *in = s; + + char token[ 128 ]; + char rawtoken[ 128 ]; + + token[ 0 ] = 0; + rawtoken[ 0 ] = 0; + + int n = 0; + + bool gt = false; + bool lt = false; + bool eq = false; + bool nt = false; + bool bit = false; + + bool done = false; + while ( !done ) + { + switch( *in ) + { + case '>': + { + gt = true; + Assert( !lt ); // Can't be both + } + break; + case '<': + { + lt = true; + Assert( !gt ); // Can't be both + } + break; + case '=': + { + eq = true; + } + break; + case ',': + case '\0': + { + rawtoken[ n ] = 0; + n = 0; + + // Convert raw token to real token in case token is an enumerated type specifier + ResolveToken( matcher, token, sizeof( token ), rawtoken ); + + // Bits are an entirely different and independent story + if (bit) + { + matcher.isbit = true; + matcher.notequal = nt; + + matcher.isnumeric = true; + } + else + // Fill in first data set + if ( gt ) + { + matcher.usemin = true; + matcher.minequals = eq; + matcher.minval = (float)atof( token ); + + matcher.isnumeric = true; + } + else if ( lt ) + { + matcher.usemax = true; + matcher.maxequals = eq; + matcher.maxval = (float)atof( token ); + + matcher.isnumeric = true; + } + else + { + if ( *in == ',' ) + { + // If there's a comma, this better have been a less than or a gt key + Assert( 0 ); + } + + matcher.notequal = nt; + + matcher.isnumeric = AppearsToBeANumber( token ); + } + + gt = lt = eq = nt = false; + + if ( !(*in) ) + { + done = true; + } + } + break; + case '!': + nt = true; + break; + case '~': + nt = true; + case '&': + bit = true; + break; + default: + rawtoken[ n++ ] = *in; + break; + } + + in++; + } + + matcher.SetToken( token ); + matcher.SetRaw( rawtoken ); + matcher.valid = true; +} + +bool CResponseSystem::CompareUsingMatcher( const char *setValue, Matcher& m, bool verbose /*=false*/ ) +{ + if ( !m.valid ) + return false; + + float v = (float)atof( setValue ); + if ( setValue[0] == '[' ) + { + bool found = false; + v = LookupEnumeration( setValue, found ); + } + + // Bits are always a different story + if (m.isbit) + { + int v1 = v; + int v2 = atoi(m.GetToken()); + if (m.notequal) + return (v1 & v2) == 0; + else + return (v1 & v2) != 0; + } + + int minmaxcount = 0; + + if ( m.usemin ) + { + if ( m.minequals ) + { + if ( v < m.minval ) + return false; + } + else + { + if ( v <= m.minval ) + return false; + } + + ++minmaxcount; + } + + if ( m.usemax ) + { + if ( m.maxequals ) + { + if ( v > m.maxval ) + return false; + } + else + { + if ( v >= m.maxval ) + return false; + } + + ++minmaxcount; + } + + // Had one or both criteria and met them + if ( minmaxcount >= 1 ) + { + return true; + } + + if ( m.notequal ) + { + if ( m.isnumeric ) + { + if ( v == (float)atof( m.GetToken() ) ) + return false; + } + else + { + if ( !Q_stricmp( setValue, m.GetToken() ) ) + return false; + } + + return true; + } + + if ( m.isnumeric ) + { + // If the setValue is "", the NPC doesn't have the key at all, + // in which case we shouldn't match "0". + if ( !setValue || !setValue[0] ) + return false; + + return v == (float)atof( m.GetToken() ); + } + + return !Q_stricmp( setValue, m.GetToken() ) ? true : false; +} + +bool CResponseSystem::Compare( const char *setValue, Criteria *c, bool verbose /*= false*/ ) +{ + Assert( c ); + Assert( setValue ); + + bool bret = CompareUsingMatcher( setValue, c->matcher, verbose ); + + if ( verbose ) + { + DevMsg( "'%20s' vs. '%20s' = ", setValue, c->value ); + + { + //DevMsg( "\n" ); + //m.Describe(); + } + } + return bret; +} + +float CResponseSystem::RecursiveScoreSubcriteriaAgainstRule( const CriteriaSet& set, Criteria *parent, bool& exclude, bool verbose /*=false*/ ) +{ + float score = 0.0f; + int subcount = parent->subcriteria.Count(); + for ( int i = 0; i < subcount; i++ ) + { + int icriterion = parent->subcriteria[ i ]; + + bool excludesubrule = false; + if (verbose) + { + DevMsg( "\n" ); + } + score += ScoreCriteriaAgainstRuleCriteria( set, icriterion, excludesubrule, verbose ); + } + + exclude = ( parent->required && score == 0.0f ) ? true : false; + + return score * parent->weight.GetFloat(); +} + +float CResponseSystem::RecursiveLookForCriteria( const CriteriaSet &criteriaSet, Criteria *pParent ) +{ + float flScore = 0.0f; + int nSubCount = pParent->subcriteria.Count(); + for ( int iSub = 0; iSub < nSubCount; ++iSub ) + { + int iCriteria = pParent->subcriteria[iSub]; + flScore += LookForCriteria( criteriaSet, iCriteria ); + } + + return flScore; +} + +float CResponseSystem::LookForCriteria( const CriteriaSet &criteriaSet, int iCriteria ) +{ + Criteria *pCriteria = &m_Criteria[iCriteria]; + if ( pCriteria->IsSubCriteriaType() ) + { + return RecursiveLookForCriteria( criteriaSet, pCriteria ); + } + + int iIndex = criteriaSet.FindCriterionIndex( pCriteria->nameSym ); + if ( iIndex == -1 ) + return 0.0f; + + Assert( criteriaSet.GetValue( iIndex ) ); + if ( Q_stricmp( criteriaSet.GetValue( iIndex ), pCriteria->value ) ) + return 0.0f; + + return 1.0f; +} + +float CResponseSystem::ScoreCriteriaAgainstRuleCriteria( const CriteriaSet& set, int icriterion, bool& exclude, bool verbose /*=false*/ ) +{ + Criteria *c = &m_Criteria[ icriterion ]; + + if ( c->IsSubCriteriaType() ) + { + return RecursiveScoreSubcriteriaAgainstRule( set, c, exclude, verbose ); + } + + if ( verbose ) + { + DevMsg( " criterion '%25s':'%15s' ", m_Criteria.GetElementName( icriterion ), CriteriaSet::SymbolToStr(c->nameSym) ); + } + + exclude = false; + + float score = 0.0f; + + const char *actualValue = ""; + + /* + const char * RESTRICT critname = c->name; + CUtlSymbol sym(critname); + const char * nameDoubleCheck = sym.String(); + */ + int found = set.FindCriterionIndex( c->nameSym ); + if ( found != -1 ) + { + actualValue = set.GetValue( found ); + if ( !actualValue ) + { + Assert( 0 ); + return score; + } + } + + Assert( actualValue ); + + if ( Compare( actualValue, c, verbose ) ) + { + float w = set.GetWeight( found ); + score = w * c->weight.GetFloat(); + + if ( verbose ) + { + DevMsg( "matched, weight %4.2f (s %4.2f x c %4.2f)", + score, w, c->weight.GetFloat() ); + } + } + else + { + if ( c->required ) + { + exclude = true; + if ( verbose ) + { + DevMsg( "failed (+exclude rule)" ); + } + } + else + { + if ( verbose ) + { + DevMsg( "failed" ); + } + } + } + + return score; +} + +float CResponseSystem::ScoreCriteriaAgainstRule( const CriteriaSet& set, ResponseRulePartition::tRuleDict &dict, int irule, bool verbose /*=false*/ ) +{ + Rule * RESTRICT rule = dict[ irule ]; + float score = 0.0f; + + bool bBeingWatched = false; + + // See if we're trying to debug this rule + const char *pszText = rr_debugrule.GetString(); + if ( pszText && pszText[0] && !Q_stricmp( pszText, dict.GetElementName( irule ) ) ) + { + bBeingWatched = true; + } + + if ( !rule->IsEnabled() ) + { + if ( bBeingWatched ) + { + DevMsg("Rule is disabled.\n" ); + } + return 0.0f; + } + + if ( bBeingWatched ) + { + verbose = true; + } + + if ( verbose ) + { + DevMsg( "Scoring rule '%s' (%i)\n{\n", dict.GetElementName( irule ), irule+1 ); + } + + // Iterate set criteria + int count = rule->m_Criteria.Count(); + int i; + for ( i = 0; i < count; i++ ) + { + int icriterion = rule->m_Criteria[ i ]; + + bool exclude = false; + score += ScoreCriteriaAgainstRuleCriteria( set, icriterion, exclude, verbose ); + + if ( verbose ) + { + DevMsg( ", score %4.2f\n", score ); + } + + if ( exclude ) + { + score = 0.0f; + break; + } + } + + if ( verbose ) + { + DevMsg( "}\n" ); + } + + if ( rule->m_nForceWeight > 0 ) + { // this means override the cumulative weight of criteria and just force the rule's total score, + // assuming it matched at all. + return fsel( score - FLT_MIN, rule->m_nForceWeight, 0 ); + } + else + { + return score; +} +} + +void CResponseSystem::DebugPrint( int depth, const char *fmt, ... ) +{ + int indentchars = 3 * depth; + char *indent = (char *) stackalloc( indentchars + 1); + indent[ indentchars ] = 0; + while ( --indentchars >= 0 ) + { + indent[ indentchars ] = ' '; + } + + // Dump text to debugging console. + va_list argptr; + char szText[1024]; + + va_start (argptr, fmt); + Q_vsnprintf (szText, sizeof( szText ), fmt, argptr); + va_end (argptr); + + DevMsg( "%s%s", indent, szText ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CResponseSystem::ResetResponseGroups() +{ + int i; + int c = m_Responses.Count(); + for ( i = 0; i < c; i++ ) + { + m_Responses[ i ].Reset(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Make certain responses unavailable by marking them as depleted +//----------------------------------------------------------------------------- +void CResponseSystem::FakeDepletes( ResponseGroup *g, IResponseFilter *pFilter ) +{ + m_FakedDepletes.RemoveAll(); + + // Fake depletion of unavailable choices + int c = g->group.Count(); + if ( pFilter && g->ShouldCheckRepeats() ) + { + for ( int i = 0; i < c; i++ ) + { + ParserResponse *r = &g->group[ i ]; + if ( r->depletioncount != g->GetDepletionCount() && !pFilter->IsValidResponse( r->GetType(), r->value ) ) + { + m_FakedDepletes.AddToTail( i ); + g->MarkResponseUsed( i ); + } + } + } + + // Fake depletion of choices that fail the odds check + for ( int i = 0; i < c; i++ ) + { + ParserResponse *r = &g->group[ i ]; + if ( RandomInt( 1, 100 ) > r->params.odds ) + { + m_FakedDepletes.AddToTail( i ); + g->MarkResponseUsed( i ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Restore responses that were faked as being depleted +//----------------------------------------------------------------------------- +void CResponseSystem::RevertFakedDepletes( ResponseGroup *g ) +{ + for ( int i = 0; i < m_FakedDepletes.Count(); i++ ) + { + g->group[ m_FakedDepletes[ i ] ].depletioncount = 0; + } + m_FakedDepletes.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *g - +// Output : int +//----------------------------------------------------------------------------- +int CResponseSystem::SelectWeightedResponseFromResponseGroup( ResponseGroup *g, IResponseFilter *pFilter ) +{ + int c = g->group.Count(); + if ( !c ) + { + Assert( !"Expecting response group with >= 1 elements" ); + return -1; + } + + FakeDepletes( g, pFilter ); + + if ( !g->HasUndepletedChoices() ) + { + g->ResetDepletionCount(); + + FakeDepletes( g, pFilter ); + + if ( !g->HasUndepletedChoices() ) + return -1; + + // Disable the group if we looped through all the way + if ( g->IsNoRepeat() ) + { + g->SetEnabled( false ); + return -1; + } + } + + bool checkrepeats = g->ShouldCheckRepeats(); + int depletioncount = g->GetDepletionCount(); + + float totalweight = 0.0f; + int slot = -1; + + if ( checkrepeats ) + { + int check= -1; + // Snag the first slot right away + if ( g->HasUndepletedFirst( check ) && check != -1 ) + { + slot = check; + } + + if ( slot == -1 && g->HasUndepletedLast( check ) && check != -1 ) + { + // If this is the only undepleted one, use it now + int i; + for ( i = 0; i < c; i++ ) + { + ParserResponse *r = &g->group[ i ]; + if ( checkrepeats && + ( r->depletioncount == depletioncount ) ) + { + continue; + } + + if ( r->last ) + { + Assert( i == check ); + continue; + } + + // There's still another undepleted entry + break; + } + + // No more undepleted so use the r->last slot + if ( i >= c ) + { + slot = check; + } + } + } + + if ( slot == -1 ) + { + for ( int i = 0; i < c; i++ ) + { + ParserResponse *r = &g->group[ i ]; + if ( checkrepeats && + ( r->depletioncount == depletioncount ) ) + { + continue; + } + + // Always skip last entry here since we will deal with it above + if ( checkrepeats && r->last ) + continue; + + int prevSlot = slot; + + if ( !totalweight ) + { + slot = i; + } + + // Always assume very first slot will match + totalweight += r->weight.GetFloat(); + if ( !totalweight || IEngineEmulator::Get()->GetRandomStream()->RandomFloat(0,totalweight) < r->weight.GetFloat() ) + { + slot = i; + } + + if ( !checkrepeats && slot != prevSlot && pFilter && !pFilter->IsValidResponse( r->GetType(), r->value ) ) + { + slot = prevSlot; + totalweight -= r->weight.GetFloat(); + } + } + } + + if ( slot != -1 ) + g->MarkResponseUsed( slot ); + + // Revert fake depletion of unavailable choices + RevertFakedDepletes( g ); + + return slot; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : searchResult - +// depth - +// *name - +// verbose - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CResponseSystem::ResolveResponse( ResponseSearchResult& searchResult, int depth, const char *name, bool verbose /*= false*/, IResponseFilter *pFilter ) +{ + int responseIndex = m_Responses.Find( name ); + if ( responseIndex == m_Responses.InvalidIndex() ) + return false; + + ResponseGroup *g = &m_Responses[ responseIndex ]; + // Group has been disabled + if ( !g->IsEnabled() ) + return false; + + int c = g->group.Count(); + if ( !c ) + return false; + + int idx = 0; + + if ( g->IsSequential() ) + { + // See if next index is valid + int initialIndex = g->GetCurrentIndex(); + bool bFoundValid = false; + + do + { + idx = g->GetCurrentIndex(); + g->SetCurrentIndex( idx + 1 ); + if ( idx >= c ) + { + if ( g->IsNoRepeat() ) + { + g->SetEnabled( false ); + return false; + } + idx = 0; + g->SetCurrentIndex( 0 ); + } + + if ( !pFilter || pFilter->IsValidResponse( g->group[idx].GetType(), g->group[idx].value ) ) + { + bFoundValid = true; + break; + } + + } while ( g->GetCurrentIndex() != initialIndex ); + + if ( !bFoundValid ) + return false; + } + else + { + idx = SelectWeightedResponseFromResponseGroup( g, pFilter ); + if ( idx < 0 ) + return false; + } + + if ( verbose ) + { + DebugPrint( depth, "%s\n", m_Responses.GetElementName( responseIndex ) ); + DebugPrint( depth, "{\n" ); + DescribeResponseGroup( g, idx, depth ); + } + + bool bret = true; + + ParserResponse *result = &g->group[ idx ]; + if ( result->type == RESPONSE_RESPONSE ) + { + // Recurse + bret = ResolveResponse( searchResult, depth + 1, result->value, verbose, pFilter ); + } + else + { + searchResult.action = result; + searchResult.group = g; + } + + if( verbose ) + { + DebugPrint( depth, "}\n" ); + } + + return bret; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *group - +// selected - +// depth - +//----------------------------------------------------------------------------- +void CResponseSystem::DescribeResponseGroup( ResponseGroup *group, int selected, int depth ) +{ + int c = group->group.Count(); + + for ( int i = 0; i < c ; i++ ) + { + ParserResponse *r = &group->group[ i ]; + DebugPrint( depth + 1, "%s%20s : %40s %5.3f\n", + i == selected ? "-> " : " ", + CRR_Response::DescribeResponse( r->GetType() ), + r->value, + r->weight.GetFloat() ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *rule - +// Output : CResponseSystem::Response +//----------------------------------------------------------------------------- +bool CResponseSystem::GetBestResponse( ResponseSearchResult& searchResult, Rule *rule, bool verbose /*=false*/, IResponseFilter *pFilter ) +{ + int c = rule->m_Responses.Count(); + if ( !c ) + return false; + + int index = IEngineEmulator::Get()->GetRandomStream()->RandomInt( 0, c - 1 ); + int groupIndex = rule->m_Responses[ index ]; + + ResponseGroup *g = &m_Responses[ groupIndex ]; + + // Group has been disabled + if ( !g->IsEnabled() ) + return false; + + int count = g->group.Count(); + if ( !count ) + return false; + + int responseIndex = 0; + + if ( g->IsSequential() ) + { + // See if next index is valid + int initialIndex = g->GetCurrentIndex(); + bool bFoundValid = false; + + do + { + responseIndex = g->GetCurrentIndex(); + g->SetCurrentIndex( responseIndex + 1 ); + if ( responseIndex >= count ) + { + if ( g->IsNoRepeat() ) + { + g->SetEnabled( false ); + return false; + } + responseIndex = 0; + g->SetCurrentIndex( 0 ); + } + + if ( !pFilter || pFilter->IsValidResponse( g->group[responseIndex].GetType(), g->group[responseIndex].value ) ) + { + bFoundValid = true; + break; + } + + } while ( g->GetCurrentIndex() != initialIndex ); + + if ( !bFoundValid ) + return false; + } + else + { + responseIndex = SelectWeightedResponseFromResponseGroup( g, pFilter ); + if ( responseIndex < 0 ) + return false; + } + + + ParserResponse *r = &g->group[ responseIndex ]; + + int depth = 0; + + if ( verbose ) + { + DebugPrint( depth, "%s\n", m_Responses.GetElementName( groupIndex ) ); + DebugPrint( depth, "{\n" ); + + DescribeResponseGroup( g, responseIndex, depth ); + } + + bool bret = true; + + if ( r->type == RESPONSE_RESPONSE ) + { + bret = ResolveResponse( searchResult, depth + 1, r->value, verbose, pFilter ); + } + else + { + searchResult.action = r; + searchResult.group = g; + } + + if ( verbose ) + { + DebugPrint( depth, "}\n" ); + } + + return bret; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : set - +// verbose - +// Output : int +// Warning: If you change this, be sure to also change +// ResponseSystemImplementationCLI::FindAllRulesMatchingCriteria(). +//----------------------------------------------------------------------------- +ResponseRulePartition::tIndex CResponseSystem::FindBestMatchingRule( const CriteriaSet& set, bool verbose, float &scoreOfBestMatchingRule ) +{ + CUtlVector< ResponseRulePartition::tIndex > bestrules(16,4); + float bestscore = 0.001f; + scoreOfBestMatchingRule = 0; + + CUtlVectorFixed< ResponseRulePartition::tRuleDict *, 2 > buckets( 0, 2 ); + m_RulePartitions.GetDictsForCriteria( &buckets, set ); + for ( int b = 0 ; b < buckets.Count() ; ++b ) + { + ResponseRulePartition::tRuleDict *prules = buckets[b]; + int c = prules->Count(); + int i; + for ( i = 0; i < c; i++ ) + { + float score = ScoreCriteriaAgainstRule( set, *prules, i, verbose ); + // Check equals so that we keep track of all matching rules + if ( score >= bestscore ) + { + // Reset bucket + if( score != bestscore ) + { + bestscore = score; + bestrules.RemoveAll(); + } + + // Add to bucket + bestrules.AddToTail( m_RulePartitions.IndexFromDictElem( prules, i ) ); + } + } + } + + int bestCount = bestrules.Count(); + if ( bestCount <= 0 ) + return m_RulePartitions.InvalidIdx(); + + scoreOfBestMatchingRule = bestscore ; + if ( bestCount == 1 ) + { + return bestrules[ 0 ] ; + } + else + { + // Randomly pick one of the tied matching rules + int idx = IEngineEmulator::Get()->GetRandomStream()->RandomInt( 0, bestCount - 1 ); + if ( verbose ) + { + DevMsg( "Found %i matching rules, selecting slot %i\n", bestCount, idx ); + } + return bestrules[ idx ] ; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : set - +// Output : CRR_Response +//----------------------------------------------------------------------------- +bool CResponseSystem::FindBestResponse( const CriteriaSet& set, CRR_Response& response, IResponseFilter *pFilter ) +{ + bool valid = false; + + int iDbgResponse = rr_debugresponses.GetInt(); + bool showRules = ( iDbgResponse >= 2 && iDbgResponse < RR_DEBUGRESPONSES_SPECIALCASE ); + bool showResult = ( iDbgResponse >= 1 && iDbgResponse < RR_DEBUGRESPONSES_SPECIALCASE ); + + // Look for match. verbose mode used to be at level 2, but disabled because the writers don't actually care for that info. + float scoreOfBestRule; + ResponseRulePartition::tIndex bestRule = FindBestMatchingRule( set, + ( iDbgResponse >= 3 && iDbgResponse < RR_DEBUGRESPONSES_SPECIALCASE ), + scoreOfBestRule ); + + ResponseType_t responseType = RESPONSE_NONE; + ResponseParams rp; + + char ruleName[ 128 ]; + char responseName[ 128 ]; + const char *context; + int iApplyContext; + ruleName[ 0 ] = 0; + responseName[ 0 ] = 0; + context = NULL; + iApplyContext = 0; + if ( m_RulePartitions.IsValid( bestRule ) ) + { + Rule * RESTRICT r = &m_RulePartitions[ bestRule ]; + + ResponseSearchResult result; + if ( GetBestResponse( result, r, showResult, pFilter ) ) + { + Q_strncpy( responseName, result.action->value, sizeof( responseName ) ); + responseType = result.action->GetType(); + rp = result.action->params; + rp.m_pFollowup = &result.action->m_followup; + } + + Q_strncpy( ruleName, m_RulePartitions.GetElementName( bestRule ), sizeof( ruleName ) ); + + // Disable the rule if it only allows for matching one time + if ( r->IsMatchOnce() ) + { + r->Disable(); + } + context = r->GetContext(); + iApplyContext = r->m_iContextFlags; + + response.SetMatchScore(scoreOfBestRule); + valid = true; + } + + response.Init( responseType, responseName, rp, ruleName, context, iApplyContext); + + if ( showResult ) + { + /* + // clipped -- chet doesn't really want this info + if ( valid ) + { + // Rescore the winner and dump to console + ScoreCriteriaAgainstRule( set, bestRule, true ); + } + */ + + + if ( valid || showRules ) + { + const char *pConceptFilter = rr_debugresponseconcept.GetString(); + // Describe the response, too + if ( V_strlen(pConceptFilter) > 0 && !rr_debugresponseconcept.GetBool() ) + { // filter for only one concept + if ( V_stricmp(pConceptFilter, set.GetValue(set.FindCriterionIndex("concept")) ) == 0 ) + { + response.Describe(&set); + } // else don't print + } + else + { + // maybe we need to filter *out* some concepts + if ( m_DebugExcludeList.IsValidIndex( m_DebugExcludeList.Head() ) ) + { + // we are excluding at least one concept + CRR_Concept test( set.GetValue(set.FindCriterionIndex("concept")) ); + if ( ! m_DebugExcludeList.IsValidIndex( m_DebugExcludeList.Find( test ) ) ) + { // if not found in exclude list, then print + response.Describe(&set); + } + } + else + { + // describe everything + response.Describe(&set); + } + } + } + } + + return valid; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CResponseSystem::GetAllResponses( CUtlVector *pResponses ) +{ + for ( int i = 0; i < (int)m_Responses.Count(); i++ ) + { + ResponseGroup &group = m_Responses[i]; + + for ( int j = 0; j < group.group.Count(); j++) + { + ParserResponse &response = group.group[j]; + if ( response.type != RESPONSE_RESPONSE ) + { + /* + CRR_Response *pResponse = new CRR_Response; + pResponse->Init( response.GetType(), response.value, CriteriaSet(), response.params, NULL, NULL, false ); + pResponses->AddToTail(pResponse); + */ + pResponses->Element(pResponses->AddToTail()).Init( response.GetType(), response.value, response.params, NULL, NULL, false ); + } + } + } +} + +void CResponseSystem::ParseInclude() +{ + char includefile[ 256 ]; + ParseToken(); + Q_snprintf( includefile, sizeof( includefile ), "scripts/%s", token ); + + // check if the file is already included + if ( m_IncludedFiles.Find( includefile ) != NULL ) + { + return; + } + + MEM_ALLOC_CREDIT(); + + // Try and load it + CUtlBuffer buf; + if ( !IEngineEmulator::Get()->GetFilesystem()->ReadFile( includefile, "GAME", buf ) ) + { + DevMsg( "Unable to load #included script %s\n", includefile ); + return; + } + + LoadFromBuffer( includefile, (const char *)buf.PeekGet() ); +} + +void CResponseSystem::LoadFromBuffer( const char *scriptfile, const char *buffer ) +{ + COM_TimestampedLog( "CResponseSystem::LoadFromBuffer [%s] - Start", scriptfile ); + m_IncludedFiles.Allocate( scriptfile ); + PushScript( scriptfile, (unsigned char * )buffer ); + + if( rr_dumpresponses.GetBool() ) + { + DevMsg("Reading: %s\n", scriptfile ); + } + + while ( 1 ) + { + ParseToken(); + if ( !token[0] ) + { + break; + } + + unsigned int hash = RR_HASH( token ); + bool bSuccess = Dispatch( token, hash, m_FileDispatch ); + if ( !bSuccess ) + { + int byteoffset = m_ScriptStack[ 0 ].currenttoken - (const char *)m_ScriptStack[ 0 ].buffer; + + Error( "CResponseSystem::LoadFromBuffer: Unknown entry type '%s', expecting 'response', 'criterion', 'enumeration' or 'rules' in file %s(offset:%i)\n", + token, scriptfile, byteoffset ); + break; + } + } + + if ( m_ScriptStack.Count() == 1 ) + { + char cur[ 256 ]; + GetCurrentScript( cur, sizeof( cur ) ); + DevMsg( 1, "CResponseSystem: %s (%i rules, %i criteria, and %i responses)\n", + cur, m_RulePartitions.Count(), m_Criteria.Count(), m_Responses.Count() ); + + if( rr_dumpresponses.GetBool() ) + { + DumpRules(); + } + } + + PopScript(); + COM_TimestampedLog( "CResponseSystem::LoadFromBuffer [%s] - Finish", scriptfile ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CResponseSystem::LoadRuleSet( const char *basescript ) +{ + float flStart = Plat_FloatTime(); + int length = 0; + unsigned char *buffer = (unsigned char *)IEngineEmulator::Get()->LoadFileForMe( basescript, &length ); + if ( length <= 0 || !buffer ) + { + DevMsg( 1, "CResponseSystem: failed to load %s\n", basescript ); + return; + } + + m_IncludedFiles.FreeAll(); + LoadFromBuffer( basescript, (const char *)buffer ); + + IEngineEmulator::Get()->FreeFile( buffer ); + + Assert( m_ScriptStack.Count() == 0 ); + float flEnd = Plat_FloatTime(); + COM_TimestampedLog( "CResponseSystem::LoadRuleSet took %f msec", 1000.0f * ( flEnd - flStart ) ); +} + +inline ResponseType_t ComputeResponseType( const char *s ) +{ + switch ( s[ 0 ] ) + { + default: + break; + case 's': + switch ( s[ 1 ] ) + { + default: + break; + case 'c': + return RESPONSE_SCENE; + case 'e': + return RESPONSE_SENTENCE; + case 'p': + return RESPONSE_SPEAK; + } + break; + case 'r': + return RESPONSE_RESPONSE; + case 'p': + return RESPONSE_PRINT; + case 'e': + return RESPONSE_ENTITYIO; + } + + return RESPONSE_NONE; +} + +void CResponseSystem::ParseResponse_Weight( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + ParseToken(); + newResponse.weight.SetFloat( (float)atof( token ) ); +} + +void CResponseSystem::ParseResponse_PreDelay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + ParseToken(); + rp->flags |= AI_ResponseParams::RG_DELAYBEFORESPEAK; + rp->predelay.FromInterval( ReadInterval( token ) ); +} + +void CResponseSystem::ParseResponse_NoDelay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + ParseToken(); + rp->flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; + rp->delay.start = 0; + rp->delay.range = 0; +} + +void CResponseSystem::ParseResponse_DefaultDelay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + rp->flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; + rp->delay.start = AIS_DEF_MIN_DELAY; + rp->delay.range = ( AIS_DEF_MAX_DELAY - AIS_DEF_MIN_DELAY ); +} + +void CResponseSystem::ParseResponse_Delay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + ParseToken(); + rp->flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; + rp->delay.FromInterval( ReadInterval( token ) ); +} + +void CResponseSystem::ParseResponse_SpeakOnce( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + rp->flags |= AI_ResponseParams::RG_SPEAKONCE; +} + +void CResponseSystem::ParseResponse_NoScene( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + rp->flags |= AI_ResponseParams::RG_DONT_USE_SCENE; +} + +void CResponseSystem::ParseResponse_StopOnNonIdle( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + rp->flags |= AI_ResponseParams::RG_STOP_ON_NONIDLE; +} + +void CResponseSystem::ParseResponse_Odds( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + ParseToken(); + rp->flags |= AI_ResponseParams::RG_ODDS; + rp->odds = clamp( atoi( token ), 0, 100 ); +} + +void CResponseSystem::ParseResponse_RespeakDelay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + ParseToken(); + rp->flags |= AI_ResponseParams::RG_RESPEAKDELAY; + rp->respeakdelay.FromInterval( ReadInterval( token ) ); +} + +void CResponseSystem::ParseResponse_WeaponDelay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + ParseToken(); + rp->flags |= AI_ResponseParams::RG_WEAPONDELAY; + rp->weapondelay.FromInterval( ReadInterval( token ) ); +} + +void CResponseSystem::ParseResponse_Soundlevel( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + ParseToken(); + rp->flags |= AI_ResponseParams::RG_SOUNDLEVEL; + rp->soundlevel = (soundlevel_t)TextToSoundLevel( token ); +} + +void CResponseSystem::ParseResponse_DisplayFirst( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + newResponse.first = true; + group.m_bHasFirst = true; +} + +void CResponseSystem::ParseResponse_DisplayLast( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + newResponse.last = true; + group.m_bHasLast= true; +} + +void CResponseSystem::ParseResponse_Fire( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + // get target name + bool bSuc = ParseToken(); + if (!bSuc) + { + ResponseWarning( "FIRE token in response needs exactly three parameters." ); + return; + } + newResponse.m_followup.followup_entityiotarget = ResponseCopyString(token); + + bSuc = ParseToken(); + if (!bSuc) + { + ResponseWarning( "FIRE token in response needs exactly three parameters." ); + return; + } + newResponse.m_followup.followup_entityioinput = ResponseCopyString(token); + + bSuc = ParseToken(); + if (!bSuc) + { + ResponseWarning( "FIRE token in response needs exactly three parameters." ); + return; + } + newResponse.m_followup.followup_entityiodelay = atof( token ); + /* + m_followup.followup_entityioinput = ResponseCopyString(src.m_followup.followup_entityioinput); + m_followup.followup_entityiotarget = ResponseCopyString(src.m_followup.followup_entityiotarget); + */ +} + +void CResponseSystem::ParseResponse_Then( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + // eg, "subject TALK_ANSWER saidunplant:1 3" + bool bSuc = ParseToken(); + if (!bSuc) + { + AssertMsg(false, "THEN token in response lacked any further info.\n"); + ResponseWarning( "THEN token in response lacked any further info.\n" ); + return; + } + + newResponse.m_followup.followup_target = ResponseCopyString(token); + + bSuc = ParseToken(); // get another token + if (!bSuc) + { + AssertMsg1(false, "THEN token in response had a target '%s', but lacked any further info.\n", newResponse.m_followup.followup_target ); + ResponseWarning( "THEN token in response had a target '%s', but lacked any further info.\n", newResponse.m_followup.followup_target ); + return; + } + + newResponse.m_followup.followup_concept = ResponseCopyString( token ); + + + // Okay, this is totally asinine. + // Because the ParseToken() function will split foo:bar into three tokens + // (which is reasonable), but we have no safe way to parse the file otherwise + // because it's all behind an engine interface, it's necessary to parse all + // the tokens to the end of the line and catenate them, except for the last one + // which is the delay. That's crap. + bSuc = ParseToken(); + if (!bSuc) + { + AssertMsg(false, "THEN token in response lacked contexts.\n"); + ResponseWarning( "THEN token in response lacked contexts.\n" ); + return; + } + + // okay, as long as there is at least one more token, catenate the ones we + // see onto a temporary buffer. When we're down to the last token, that is + // the delay. + char buf[4096]; + buf[0] = '\0'; + while ( TokenWaiting() ) + { + Q_strncat( buf, token, 4096 ); + bSuc = ParseToken(); + AssertMsg(bSuc, "Token parsing mysteriously failed."); + } + + // down here, token is the last token, and buf is everything up to there. + newResponse.m_followup.followup_contexts = ResponseCopyString( buf ); + + newResponse.m_followup.followup_delay = atof( token ); +} + +void CResponseSystem::ParseOneResponse( const char *responseGroupName, ResponseGroup& group, ResponseParams *defaultParams ) +{ + ParserResponse &newResponse = group.group[ group.group.AddToTail() ]; + newResponse.weight.SetFloat( 1.0f ); + // inherit from group if appropriate + if (defaultParams) + { + newResponse.params = *defaultParams; + } + + ResponseParams *rp = &newResponse.params; + + newResponse.type = ComputeResponseType( token ); + if ( RESPONSE_NONE == newResponse.type ) +{ + ResponseWarning( "response entry '%s' with unknown response type '%s'\n", responseGroupName, token ); + return; +} + + ParseToken(); + newResponse.value = ResponseCopyString( token ); + + while ( TokenWaiting() ) + { + ParseToken(); + + unsigned int hash = RR_HASH( token ); + if ( DispatchParseResponse( token, hash, m_ResponseDispatch, newResponse, group, rp ) ) + { + continue; + } + + ResponseWarning( "response entry '%s' with unknown command '%s'\n", responseGroupName, token ); + } + +} + +void CResponseSystem::ParseResponseGroup_Start( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + while ( 1 ) + { + ParseToken(); + if ( !Q_stricmp( token, "}" ) ) + break; + + if ( !Q_stricmp( token, "permitrepeats" ) ) + { + newGroup.m_bDepleteBeforeRepeat = false; + continue; + } + else if ( !Q_stricmp( token, "sequential" ) ) + { + newGroup.SetSequential( true ); + continue; + } + else if ( !Q_stricmp( token, "norepeat" ) ) + { + newGroup.SetNoRepeat( true ); + continue; + } + + ParseOneResponse( responseGroupName, newGroup ); + } + } + +void CResponseSystem::ParseResponseGroup_PreDelay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + ParseToken(); + groupResponseParams.flags |= AI_ResponseParams::RG_DELAYBEFORESPEAK; + groupResponseParams.predelay.FromInterval( ReadInterval( token ) ); + } + +void CResponseSystem::ParseResponseGroup_NoDelay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + ParseToken(); + groupResponseParams.flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; + groupResponseParams.delay.start = 0; + groupResponseParams.delay.range = 0; + } + +void CResponseSystem::ParseResponseGroup_DefaultDelay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + groupResponseParams.flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; + groupResponseParams.delay.start = AIS_DEF_MIN_DELAY; + groupResponseParams.delay.range = ( AIS_DEF_MAX_DELAY - AIS_DEF_MIN_DELAY ); + } + +void CResponseSystem::ParseResponseGroup_Delay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + ParseToken(); + groupResponseParams.flags |= AI_ResponseParams::RG_DELAYAFTERSPEAK; + groupResponseParams.delay.FromInterval( ReadInterval( token ) ); + } + +void CResponseSystem::ParseResponseGroup_SpeakOnce( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + groupResponseParams.flags |= AI_ResponseParams::RG_SPEAKONCE; + } + +void CResponseSystem::ParseResponseGroup_NoScene( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + groupResponseParams.flags |= AI_ResponseParams::RG_DONT_USE_SCENE; + } + +void CResponseSystem::ParseResponseGroup_StopOnNonIdle( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + groupResponseParams.flags |= AI_ResponseParams::RG_STOP_ON_NONIDLE; + } + +void CResponseSystem::ParseResponseGroup_Odds( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + ParseToken(); + groupResponseParams.flags |= AI_ResponseParams::RG_ODDS; + groupResponseParams.odds = clamp( atoi( token ), 0, 100 ); + } + +void CResponseSystem::ParseResponseGroup_RespeakDelay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + ParseToken(); + groupResponseParams.flags |= AI_ResponseParams::RG_RESPEAKDELAY; + groupResponseParams.respeakdelay.FromInterval( ReadInterval( token ) ); + } + +void CResponseSystem::ParseResponseGroup_WeaponDelay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + ParseToken(); + groupResponseParams.flags |= AI_ResponseParams::RG_WEAPONDELAY; + groupResponseParams.weapondelay.FromInterval( ReadInterval( token ) ); + } + +void CResponseSystem::ParseResponseGroup_Soundlevel( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ) + { + ParseToken(); + groupResponseParams.flags |= AI_ResponseParams::RG_SOUNDLEVEL; + groupResponseParams.soundlevel = (soundlevel_t)TextToSoundLevel( token ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CResponseSystem::ParseResponse( void ) +{ + AI_ResponseParams groupResponseParams; // default response parameters inherited from single line format for group + + // Should have groupname at start + ParseToken(); + char responseGroupName[ 128 ]; + Q_strncpy( responseGroupName, token, sizeof( responseGroupName ) ); + + int slot = m_Responses.Insert( responseGroupName ); + ResponseGroup &newGroup = m_Responses[ slot ]; + + while ( 1 ) + { + ParseToken(); + + unsigned int hash = RR_HASH( token ); + + // Oops, part of next definition + if( IsRootCommand( hash ) ) + { + Unget(); + break; + } + + if ( DispatchParseResponseGroup( token, hash, m_ResponseGroupDispatch, responseGroupName, newGroup, groupResponseParams ) ) + { + continue; + } + ParseOneResponse( responseGroupName, newGroup ); + } + } + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *criterion - +//----------------------------------------------------------------------------- +int CResponseSystem::ParseOneCriterion( const char *criterionName ) +{ + char key[ 128 ]; + char value[ 128 ]; + + Criteria *pNewCriterion = NULL; + + int idx; + if ( m_Criteria.Find( criterionName ) != m_Criteria.InvalidIndex() ) + { + static Criteria dummy; + pNewCriterion = &dummy; + + ResponseWarning( "Multiple definitions for criteria '%s' [%d]\n", criterionName, RR_HASH( criterionName ) ); + idx = m_Criteria.InvalidIndex(); + } + else + { + idx = m_Criteria.Insert( criterionName ); + pNewCriterion = &m_Criteria[ idx ]; + } + + bool gotbody = false; + + while ( TokenWaiting() || !gotbody ) + { + ParseToken(); + + // Oops, part of next definition + if( IsRootCommand() ) + { + Unget(); + break; + } + + if ( !Q_stricmp( token, "{" ) ) + { + gotbody = true; + + while ( 1 ) + { + ParseToken(); + if ( !Q_stricmp( token, "}" ) ) + break; + + // Look up subcriteria index + int idx = m_Criteria.Find( token ); + if ( idx != m_Criteria.InvalidIndex() ) + { + pNewCriterion->subcriteria.AddToTail( idx ); + } + else + { + ResponseWarning( "Skipping unrecongized subcriterion '%s' in '%s'\n", token, criterionName ); + } + } + continue; + } + else if ( !Q_stricmp( token, "required" ) ) + { + pNewCriterion->required = true; + } + else if ( !Q_stricmp( token, "weight" ) ) + { + ParseToken(); + pNewCriterion->weight.SetFloat( (float)atof( token ) ); + } + else + { + Assert( pNewCriterion->subcriteria.Count() == 0 ); + + // Assume it's the math info for a non-subcriteria resposne + Q_strncpy( key, token, sizeof( key ) ); + ParseToken(); + Q_strncpy( value, token, sizeof( value ) ); + + V_strlower( key ); + pNewCriterion->nameSym = CriteriaSet::ComputeCriteriaSymbol( key ); + pNewCriterion->value = ResponseCopyString( value ); + + gotbody = true; + } + } + + if ( !pNewCriterion->IsSubCriteriaType() ) + { + ComputeMatcher( pNewCriterion, pNewCriterion->matcher ); + } + + return idx; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *kv - +//----------------------------------------------------------------------------- +void CResponseSystem::ParseCriterion( void ) +{ + // Should have groupname at start + char criterionName[ 128 ]; + ParseToken(); + Q_strncpy( criterionName, token, sizeof( criterionName ) ); + + ParseOneCriterion( criterionName ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *kv - +//----------------------------------------------------------------------------- +void CResponseSystem::ParseEnumeration( void ) +{ + char enumerationName[ 128 ]; + ParseToken(); + Q_strncpy( enumerationName, token, sizeof( enumerationName ) ); + + ParseToken(); + if ( Q_stricmp( token, "{" ) ) + { + ResponseWarning( "Expecting '{' in enumeration '%s', got '%s'\n", enumerationName, token ); + return; + } + + while ( 1 ) + { + ParseToken(); + if ( !Q_stricmp( token, "}" ) ) + break; + + if ( Q_strlen( token ) <= 0 ) + { + ResponseWarning( "Expecting more tokens in enumeration '%s'\n", enumerationName ); + break; + } + + char key[ 128 ]; + + Q_strncpy( key, token, sizeof( key ) ); + ParseToken(); + float value = (float)atof( token ); + + char sz[ 128 ]; + Q_snprintf( sz, sizeof( sz ), "[%s::%s]", enumerationName, key ); + Q_strlower( sz ); + + Enumeration newEnum; + newEnum.value = value; + + if ( m_Enumerations.Find( sz ) == m_Enumerations.InvalidIndex() ) + { + m_Enumerations.Insert( sz, newEnum ); + } + /* + else + { + ResponseWarning( "Ignoring duplication enumeration '%s'\n", sz ); + } + */ + } +} + +void CResponseSystem::ParseRule_MatchOnce( Rule &newRule ) + { + newRule.m_bMatchOnce = true; + } + +void CResponseSystem::ParseRule_ApplyContextToWorld( Rule &newRule ) + { + newRule.m_iContextFlags |= APPLYCONTEXT_WORLD; + } + +void ResponseRules::CResponseSystem::ParseRule_ApplyContextToSelf(Rule& newRule) +{ + newRule.m_iContextFlags |= APPLYCONTEXT_SELF; +} + +void ResponseRules::CResponseSystem::ParseRule_ApplyContextToSquad(Rule& newRule) +{ + newRule.m_iContextFlags |= APPLYCONTEXT_SQUAD; +} + +void ResponseRules::CResponseSystem::ParseRule_ApplyContextToEnemy(Rule& newRule) +{ + newRule.m_iContextFlags |= APPLYCONTEXT_ENEMY; +} + +void CResponseSystem::ParseRule_ApplyContext( Rule &newRule ) + { + ParseToken(); + if ( newRule.GetContext() == NULL ) + { + newRule.SetContext( token ); + } + else + { + CFmtStrN<1024> newContext( "%s,%s", newRule.GetContext(), token ); + newRule.SetContext( newContext ); + } + } + +void CResponseSystem::ParseRule_Response( Rule &newRule ) + { + // Read them until we run out. + while ( TokenWaiting() ) + { + ParseToken(); + int idx = m_Responses.Find( token ); + if ( idx != m_Responses.InvalidIndex() ) + { + MEM_ALLOC_CREDIT(); + newRule.m_Responses.AddToTail( idx ); + } + else + { + m_bParseRuleValid = false; + ResponseWarning( "No such response '%s' for rule '%s'\n", token, m_pParseRuleName ); + } + } +} + +/* +void CResponseSystem::ParseRule_ForceWeight( Rule &newRule ) +{ + ParseToken(); + if ( token[0] == 0 ) + { + // no token followed forceweight? + ResponseWarning( "Forceweight token in rule '%s' did not specify a numerical weight! Ignoring.\n", m_pParseRuleName ); + } + else + { + newRule.m_nForceWeight = atoi(token); + if ( newRule.m_nForceWeight == 0 ) + { + ResponseWarning( "Rule '%s' had forceweight '%s', which doesn't work out to a nonzero number. Ignoring.\n", + m_pParseRuleName, token ); + } + } + } +*/ + +void CResponseSystem::ParseRule_Criteria( Rule &newRule ) + { + // Read them until we run out. + while ( TokenWaiting() ) + { + ParseToken(); + + int idx = m_Criteria.Find( token ); + if ( idx != m_Criteria.InvalidIndex() ) + { + MEM_ALLOC_CREDIT(); + newRule.m_Criteria.AddToTail( idx ); + } + else + { + m_bParseRuleValid = false; + ResponseWarning( "No such criterion '%s' for rule '%s'\n", token, m_pParseRuleName ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *kv - +//----------------------------------------------------------------------------- +void CResponseSystem::ParseRule( void ) +{ + static int instancedCriteria = 0; + + char ruleName[ 128 ]; + ParseToken(); + Q_strncpy( ruleName, token, sizeof( ruleName ) ); + + ParseToken(); + if ( Q_stricmp( token, "{" ) ) + { + ResponseWarning( "Expecting '{' in rule '%s', got '%s'\n", ruleName, token ); + return; + } + + // entries are "criteria", "response" or an in-line criteria to instance + Rule *newRule = new Rule; + + char sz[ 128 ]; + + m_bParseRuleValid = true; + m_pParseRuleName = ruleName; + while ( 1 ) + { + ParseToken(); + if ( !Q_stricmp( token, "}" ) ) + { + break; + } + + if ( Q_strlen( token ) <= 0 ) + { + ResponseWarning( "Expecting more tokens in rule '%s'\n", ruleName ); + break; + } + + unsigned int hash = RR_HASH( token ); + if ( DispatchParseRule( token, hash, m_RuleDispatch, *newRule ) ) + continue; + + // It's an inline criteria, generate a name and parse it in + Q_snprintf( sz, sizeof( sz ), "[%s%03i]", ruleName, ++instancedCriteria ); + Unget(); + int idx = ParseOneCriterion( sz ); + if ( idx != m_Criteria.InvalidIndex() ) + { + newRule->m_Criteria.AddToTail( idx ); + } + } + + if ( m_bParseRuleValid ) + { + m_RulePartitions.GetDictForRule( this, newRule ).Insert( ruleName, newRule ); + } + else + { + DevMsg( "Discarded rule %s\n", ruleName ); + delete newRule; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CResponseSystem::GetCurrentToken() const +{ + if ( m_ScriptStack.Count() <= 0 ) + return -1; + + return m_ScriptStack[ 0 ].tokencount; +} + + +void CResponseSystem::ResponseWarning( const char *fmt, ... ) +{ + va_list argptr; + char string[1024]; + + va_start (argptr, fmt); + Q_vsnprintf(string, sizeof(string), fmt,argptr); + va_end (argptr); + + char cur[ 256 ]; + GetCurrentScript( cur, sizeof( cur ) ); + DevMsg( 1, "%s(token %i) : %s", cur, GetCurrentToken(), string ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CResponseSystem::CopyCriteriaFrom( Rule *pSrcRule, Rule *pDstRule, CResponseSystem *pCustomSystem ) +{ + // Add criteria from this rule to global list in custom response system. + int nCriteriaCount = pSrcRule->m_Criteria.Count(); + for ( int iCriteria = 0; iCriteria < nCriteriaCount; ++iCriteria ) + { + int iSrcIndex = pSrcRule->m_Criteria[iCriteria]; + Criteria *pSrcCriteria = &m_Criteria[iSrcIndex]; + if ( pSrcCriteria ) + { + int iIndex = pCustomSystem->m_Criteria.Find( m_Criteria.GetElementName( iSrcIndex ) ); + if ( iIndex != pCustomSystem->m_Criteria.InvalidIndex() ) + { + pDstRule->m_Criteria.AddToTail( iIndex ); + continue; + } + + // Add the criteria. + Criteria dstCriteria; + + dstCriteria.nameSym = pSrcCriteria->nameSym ; + dstCriteria.value = ResponseCopyString( pSrcCriteria->value ); + dstCriteria.weight = pSrcCriteria->weight; + dstCriteria.required = pSrcCriteria->required; + dstCriteria.matcher = pSrcCriteria->matcher; + + int nSubCriteriaCount = pSrcCriteria->subcriteria.Count(); + for ( int iSubCriteria = 0; iSubCriteria < nSubCriteriaCount; ++iSubCriteria ) + { + int iSrcSubIndex = pSrcCriteria->subcriteria[iSubCriteria]; + Criteria *pSrcSubCriteria = &m_Criteria[iSrcSubIndex]; + if ( pSrcCriteria ) + { + int iSubIndex = pCustomSystem->m_Criteria.Find( pSrcSubCriteria->value ); + if ( iSubIndex != pCustomSystem->m_Criteria.InvalidIndex() ) + continue; + + // Add the criteria. + Criteria dstSubCriteria; + + dstSubCriteria.nameSym = pSrcSubCriteria->nameSym ; + dstSubCriteria.value = ResponseCopyString( pSrcSubCriteria->value ); + dstSubCriteria.weight = pSrcSubCriteria->weight; + dstSubCriteria.required = pSrcSubCriteria->required; + dstSubCriteria.matcher = pSrcSubCriteria->matcher; + + int iSubInsertIndex = pCustomSystem->m_Criteria.Insert( pSrcSubCriteria->value, dstSubCriteria ); + dstCriteria.subcriteria.AddToTail( iSubInsertIndex ); + } + } + + int iInsertIndex = pCustomSystem->m_Criteria.Insert( m_Criteria.GetElementName( iSrcIndex ), dstCriteria ); + pDstRule->m_Criteria.AddToTail( iInsertIndex ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CResponseSystem::CopyResponsesFrom( Rule *pSrcRule, Rule *pDstRule, CResponseSystem *pCustomSystem ) +{ + // Add responses from this rule to global list in custom response system. + int nResponseGroupCount = pSrcRule->m_Responses.Count(); + for ( int iResponseGroup = 0; iResponseGroup < nResponseGroupCount; ++iResponseGroup ) + { + int iSrcResponseGroup = pSrcRule->m_Responses[iResponseGroup]; + ResponseGroup *pSrcResponseGroup = &m_Responses[iSrcResponseGroup]; + if ( pSrcResponseGroup ) + { + // Add response group. + ResponseGroup dstResponseGroup; + + dstResponseGroup.m_bDepleteBeforeRepeat = pSrcResponseGroup->m_bDepleteBeforeRepeat; + dstResponseGroup.m_nDepletionCount = pSrcResponseGroup->m_nDepletionCount; + dstResponseGroup.m_bHasFirst = pSrcResponseGroup->m_bHasFirst; + dstResponseGroup.m_bHasLast = pSrcResponseGroup->m_bHasLast; + dstResponseGroup.m_bSequential = pSrcResponseGroup->m_bSequential; + dstResponseGroup.m_bNoRepeat = pSrcResponseGroup->m_bNoRepeat; + dstResponseGroup.m_bEnabled = pSrcResponseGroup->m_bEnabled; + dstResponseGroup.m_nCurrentIndex = pSrcResponseGroup->m_nCurrentIndex; + + int nSrcResponseCount = pSrcResponseGroup->group.Count(); + for ( int iResponse = 0; iResponse < nSrcResponseCount; ++iResponse ) + { + ParserResponse *pSrcResponse = &pSrcResponseGroup->group[iResponse]; + if ( pSrcResponse ) + { + // Add Response + ParserResponse dstResponse; + + dstResponse.weight = pSrcResponse->weight; + dstResponse.type = pSrcResponse->type; + dstResponse.value = ResponseCopyString( pSrcResponse->value ); + dstResponse.depletioncount = pSrcResponse->depletioncount; + dstResponse.first = pSrcResponse->first; + dstResponse.last = pSrcResponse->last; + + dstResponseGroup.group.AddToTail( dstResponse ); + } + } + + int iInsertIndex = pCustomSystem->m_Responses.Insert( m_Responses.GetElementName( iSrcResponseGroup ), dstResponseGroup ); + pDstRule->m_Responses.AddToTail( iInsertIndex ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CResponseSystem::CopyEnumerationsFrom( CResponseSystem *pCustomSystem ) +{ + int nEnumerationCount = m_Enumerations.Count(); + for ( int iEnumeration = 0; iEnumeration < nEnumerationCount; ++iEnumeration ) + { + Enumeration *pSrcEnumeration = &m_Enumerations[iEnumeration]; + if ( pSrcEnumeration ) + { + Enumeration dstEnumeration; + dstEnumeration.value = pSrcEnumeration->value; + pCustomSystem->m_Enumerations.Insert( m_Enumerations.GetElementName( iEnumeration ), dstEnumeration ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CResponseSystem::CopyRuleFrom( Rule *pSrcRule, ResponseRulePartition::tIndex iRule, CResponseSystem *pCustomSystem ) +{ + // Verify data. + Assert( pSrcRule ); + Assert( pCustomSystem ); + if ( !pSrcRule || !pCustomSystem ) + return; + + // New rule + Rule *dstRule = new Rule; + + dstRule->SetContext( pSrcRule->GetContext() ); + dstRule->m_bMatchOnce = pSrcRule->m_bMatchOnce; + dstRule->m_bEnabled = pSrcRule->m_bEnabled; + dstRule->m_iContextFlags = pSrcRule->m_iContextFlags; + + // Copy off criteria. + CopyCriteriaFrom( pSrcRule, dstRule, pCustomSystem ); + + // Copy off responses. + CopyResponsesFrom( pSrcRule, dstRule, pCustomSystem ); + + // Copy off enumerations - Don't think we use these. + // CopyEnumerationsFrom( pCustomSystem ); + + // Add rule. + pCustomSystem->m_RulePartitions.GetDictForRule( this, dstRule ).Insert( m_RulePartitions.GetElementName( iRule ), dstRule ); +} + + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CResponseSystem::DumpRules() +{ + for ( ResponseRulePartition::tIndex idx = m_RulePartitions.First() ; + m_RulePartitions.IsValid(idx) ; + idx = m_RulePartitions.Next(idx) ) + { + Msg("%s\n", m_RulePartitions.GetElementName( idx ) ); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CResponseSystem::DumpDictionary( const char *pszName ) +{ + Msg( "\nDictionary: %s\n", pszName ); + + // int nRuleCount = m_Rules.Count(); + // for ( int iRule = 0; iRule < nRuleCount; ++iRule ) + for ( ResponseRulePartition::tIndex idx = m_RulePartitions.First() ; + m_RulePartitions.IsValid(idx) ; + idx = m_RulePartitions.Next(idx) ) + { + Msg(" Rule %d/%d: %s\n", m_RulePartitions.BucketFromIdx(idx), m_RulePartitions.PartFromIdx( idx ), m_RulePartitions.GetElementName( idx ) ); + + Rule *pRule = &m_RulePartitions[idx]; + + int nCriteriaCount = pRule->m_Criteria.Count(); + for( int iCriteria = 0; iCriteria < nCriteriaCount; ++iCriteria ) + { + int iRuleCriteria = pRule->m_Criteria[iCriteria]; + Criteria *pCriteria = &m_Criteria[iRuleCriteria]; + Msg( " Criteria %d: %s %s\n", iCriteria, CriteriaSet::SymbolToStr(pCriteria->nameSym), pCriteria->value ); + } + + int nResponseGroupCount = pRule->m_Responses.Count(); + for ( int iResponseGroup = 0; iResponseGroup < nResponseGroupCount; ++iResponseGroup ) + { + int iRuleResponse = pRule->m_Responses[iResponseGroup]; + ResponseGroup *pResponseGroup = &m_Responses[iRuleResponse]; + + Msg( " ResponseGroup %d: %s\n", iResponseGroup, m_Responses.GetElementName( iRuleResponse ) ); + + int nResponseCount = pResponseGroup->group.Count(); + for ( int iResponse = 0; iResponse < nResponseCount; ++iResponse ) + { + ParserResponse *pResponse = &pResponseGroup->group[iResponse]; + Msg( " Response %d: %s\n", iResponse, pResponse->value ); + } + } + } +} + +void CResponseSystem::BuildDispatchTables() +{ + m_RootCommandHashes.Insert( RR_HASH( "#include" ) ); + m_RootCommandHashes.Insert( RR_HASH( "response" ) ); + m_RootCommandHashes.Insert( RR_HASH( "enumeration" ) ); + m_RootCommandHashes.Insert( RR_HASH( "criterion" ) ); + m_RootCommandHashes.Insert( RR_HASH( "criteria" ) ); + m_RootCommandHashes.Insert( RR_HASH( "rule" ) ); + + m_FileDispatch.Insert( RR_HASH( "#include" ), &CResponseSystem::ParseInclude ); + m_FileDispatch.Insert( RR_HASH( "response" ), &CResponseSystem::ParseResponse ); + m_FileDispatch.Insert( RR_HASH( "criterion" ), &CResponseSystem::ParseCriterion ); + m_FileDispatch.Insert( RR_HASH( "criteria" ), &CResponseSystem::ParseCriterion ); + m_FileDispatch.Insert( RR_HASH( "rule" ), &CResponseSystem::ParseRule ); + m_FileDispatch.Insert( RR_HASH( "enumeration" ), &CResponseSystem::ParseEnumeration ); + + m_RuleDispatch.Insert( RR_HASH( "matchonce" ), &CResponseSystem::ParseRule_MatchOnce ); + m_RuleDispatch.Insert( RR_HASH( "applycontexttoworld" ), &CResponseSystem::ParseRule_ApplyContextToWorld ); + m_RuleDispatch.Insert(RR_HASH("applycontexttoself"), &CResponseSystem::ParseRule_ApplyContextToSelf); + m_RuleDispatch.Insert(RR_HASH("applycontexttosquad"), &CResponseSystem::ParseRule_ApplyContextToSquad); + m_RuleDispatch.Insert(RR_HASH("applycontexttoenemy"), &CResponseSystem::ParseRule_ApplyContextToEnemy); + m_RuleDispatch.Insert( RR_HASH( "applycontext" ), &CResponseSystem::ParseRule_ApplyContext ); + m_RuleDispatch.Insert( RR_HASH( "response" ), &CResponseSystem::ParseRule_Response ); +// m_RuleDispatch.Insert( RR_HASH( "forceweight" ), &CResponseSystem::ParseRule_ForceWeight ); + m_RuleDispatch.Insert( RR_HASH( "criteria" ), &CResponseSystem::ParseRule_Criteria ); + m_RuleDispatch.Insert( RR_HASH( "criterion" ), &CResponseSystem::ParseRule_Criteria ); + + + m_ResponseDispatch.Insert( RR_HASH( "weight" ), &CResponseSystem::ParseResponse_Weight ); + m_ResponseDispatch.Insert( RR_HASH( "predelay" ), &CResponseSystem::ParseResponse_PreDelay ); + m_ResponseDispatch.Insert( RR_HASH( "nodelay" ), &CResponseSystem::ParseResponse_NoDelay ); + m_ResponseDispatch.Insert( RR_HASH( "defaultdelay" ), &CResponseSystem::ParseResponse_DefaultDelay ); + m_ResponseDispatch.Insert( RR_HASH( "delay" ), &CResponseSystem::ParseResponse_Delay ); + m_ResponseDispatch.Insert( RR_HASH( "speakonce" ), &CResponseSystem::ParseResponse_SpeakOnce ); + m_ResponseDispatch.Insert( RR_HASH( "noscene" ), &CResponseSystem::ParseResponse_NoScene ); + m_ResponseDispatch.Insert( RR_HASH( "stop_on_nonidle" ), &CResponseSystem::ParseResponse_StopOnNonIdle ); + m_ResponseDispatch.Insert( RR_HASH( "odds" ), &CResponseSystem::ParseResponse_Odds ); + m_ResponseDispatch.Insert( RR_HASH( "respeakdelay" ), &CResponseSystem::ParseResponse_RespeakDelay ); + m_ResponseDispatch.Insert( RR_HASH( "weapondelay" ), &CResponseSystem::ParseResponse_WeaponDelay ); + m_ResponseDispatch.Insert( RR_HASH( "soundlevel" ), &CResponseSystem::ParseResponse_Soundlevel ); + m_ResponseDispatch.Insert( RR_HASH( "displayfirst" ), &CResponseSystem::ParseResponse_DisplayFirst ); + m_ResponseDispatch.Insert( RR_HASH( "displaylast" ), &CResponseSystem::ParseResponse_DisplayLast ); + m_ResponseDispatch.Insert( RR_HASH( "fire" ), &CResponseSystem::ParseResponse_Fire ); + m_ResponseDispatch.Insert( RR_HASH( "then" ), &CResponseSystem::ParseResponse_Then ); + + m_ResponseGroupDispatch.Insert( RR_HASH( "{" ), &CResponseSystem::ParseResponseGroup_Start ); + m_ResponseGroupDispatch.Insert( RR_HASH( "predelay" ), &CResponseSystem::ParseResponseGroup_PreDelay ); + m_ResponseGroupDispatch.Insert( RR_HASH( "nodelay" ), &CResponseSystem::ParseResponseGroup_NoDelay ); + m_ResponseGroupDispatch.Insert( RR_HASH( "defaultdelay" ), &CResponseSystem::ParseResponseGroup_DefaultDelay ); + m_ResponseGroupDispatch.Insert( RR_HASH( "delay" ), &CResponseSystem::ParseResponseGroup_Delay ); + m_ResponseGroupDispatch.Insert( RR_HASH( "speakonce" ), &CResponseSystem::ParseResponseGroup_SpeakOnce ); + m_ResponseGroupDispatch.Insert( RR_HASH( "noscene" ), &CResponseSystem::ParseResponseGroup_NoScene ); + m_ResponseGroupDispatch.Insert( RR_HASH( "stop_on_nonidle" ), &CResponseSystem::ParseResponseGroup_StopOnNonIdle ); + m_ResponseGroupDispatch.Insert( RR_HASH( "odds" ), &CResponseSystem::ParseResponseGroup_Odds ); + m_ResponseGroupDispatch.Insert( RR_HASH( "respeakdelay" ), &CResponseSystem::ParseResponseGroup_RespeakDelay ); + m_ResponseGroupDispatch.Insert( RR_HASH( "weapondelay" ), &CResponseSystem::ParseResponseGroup_WeaponDelay ); + m_ResponseGroupDispatch.Insert( RR_HASH( "soundlevel" ), &CResponseSystem::ParseResponseGroup_Soundlevel ); +} + +bool CResponseSystem::Dispatch( char const *pToken, unsigned int uiHash, CResponseSystem::DispatchMap_t &rMap ) +{ + int slot = rMap.Find( uiHash ); + if ( slot != rMap.InvalidIndex() ) + { + CResponseSystem::pfnResponseDispatch dispatch = rMap[ slot ]; + (this->*dispatch)(); + return true; + } + + return false; +} + +bool CResponseSystem::DispatchParseRule( char const *pToken, unsigned int uiHash, ParseRuleDispatchMap_t &rMap, Rule &newRule ) +{ + int slot = rMap.Find( uiHash ); + if ( slot != rMap.InvalidIndex() ) + { + CResponseSystem::pfnParseRuleDispatch dispatch = rMap[ slot ]; + (this->*dispatch)( newRule ); + return true; + } + + return false; +} + +bool CResponseSystem::DispatchParseResponse( char const *pToken, unsigned int uiHash, ParseResponseDispatchMap_t &rMap, ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ) +{ + int slot = rMap.Find( uiHash ); + if ( slot != rMap.InvalidIndex() ) + { + CResponseSystem::pfnParseResponseDispatch dispatch = rMap[ slot ]; + (this->*dispatch)( newResponse, group, rp ); + return true; + } + + return false; +} + +bool CResponseSystem::DispatchParseResponseGroup( char const *pToken, unsigned int uiHash, ParseResponseGroupDispatchMap_t &rMap, char const *responseGroupName, ResponseGroup& newGroup, AI_ResponseParams &groupResponseParams ) +{ + int slot = rMap.Find( uiHash ); + if ( slot != rMap.InvalidIndex() ) + { + CResponseSystem::pfnParseResponseGroupDispatch dispatch = rMap[ slot ]; + (this->*dispatch)( responseGroupName, newGroup, groupResponseParams ); + return true; + } + + return false; +} + +unsigned int ResponseRulePartition::GetBucketForSpeakerAndConcept( const char *pszSpeaker, const char *pszConcept, const char *pszSubject ) +{ + // make sure is a power of two + COMPILE_TIME_ASSERT( ( N_RESPONSE_PARTITIONS & ( N_RESPONSE_PARTITIONS - 1 ) ) == 0 ); + + // hash together the speaker and concept strings, and mask off by the bucket mask + unsigned hashSpeaker = 0; // pszSpeaker ? HashStringCaseless( pszSpeaker ) : 0; + unsigned hashConcept = pszConcept ? HashStringCaseless( pszConcept ) : 0; + unsigned hashSubject = pszSubject ? HashStringCaseless( pszSubject ) : 0; + unsigned hashBrowns = ( ( hashSubject >> 3 ) ^ (hashSpeaker >> 1) ^ hashConcept ) & ( N_RESPONSE_PARTITIONS - 1 ); + return hashBrowns; +} + +const char *Rule::GetValueForRuleCriterionByName( CResponseSystem * RESTRICT pSystem, const CUtlSymbol &pCritNameSym ) +{ + const char * retval = NULL; + // for each rule criterion... + for ( int i = 0 ; i < m_Criteria.Count() ; ++i ) + { + retval = RecursiveGetValueForRuleCriterionByName( pSystem, &pSystem->m_Criteria[m_Criteria[i]], pCritNameSym ); + if ( retval != NULL ) + { + // we found a result, early out + break; + } + } + + return retval; +} + +const Criteria *Rule::GetPointerForRuleCriterionByName( CResponseSystem *pSystem, const CUtlSymbol &pCritNameSym ) +{ + const Criteria * retval = NULL; + // for each rule criterion... + for ( int i = 0 ; i < m_Criteria.Count() ; ++i ) + { + retval = RecursiveGetPointerForRuleCriterionByName( pSystem, &pSystem->m_Criteria[m_Criteria[i]], pCritNameSym ); + if ( retval != NULL ) + { + // we found a result, early out + break; + } + } + + return retval; +} + +const char *Rule::RecursiveGetValueForRuleCriterionByName( CResponseSystem * RESTRICT pSystem, + const Criteria * RESTRICT pCrit, const CUtlSymbol &pCritNameSym ) +{ + Assert( pCrit ); + if ( !pCrit ) return NULL; + if ( pCrit->IsSubCriteriaType() ) + { + // test each of the children (depth first) + const char *pRet = NULL; + for ( int i = 0 ; i < pCrit->subcriteria.Count() ; ++i ) + { + pRet = RecursiveGetValueForRuleCriterionByName( pSystem, &pSystem->m_Criteria[pCrit->subcriteria[i]], pCritNameSym ); + if ( pRet ) // if found something, early out + return pRet; + } + } + else // leaf criterion + { + if ( pCrit->nameSym == pCritNameSym ) + { + return pCrit->value; + } + else + { + return NULL; + } + } + + return NULL; +} + + +const Criteria *Rule::RecursiveGetPointerForRuleCriterionByName( CResponseSystem *pSystem, const Criteria *pCrit, const CUtlSymbol &pCritNameSym ) +{ + Assert( pCrit ); + if ( !pCrit ) return NULL; + if ( pCrit->IsSubCriteriaType() ) + { + // test each of the children (depth first) + const Criteria *pRet = NULL; + for ( int i = 0 ; i < pCrit->subcriteria.Count() ; ++i ) + { + pRet = RecursiveGetPointerForRuleCriterionByName( pSystem, &pSystem->m_Criteria[pCrit->subcriteria[i]], pCritNameSym ); + if ( pRet ) // if found something, early out + return pRet; + } + } + else // leaf criterion + { + if ( pCrit->nameSym == pCritNameSym ) + { + return pCrit; + } + else + { + return NULL; + } + } + + return NULL; +} + + +static void CC_RR_Debug_ResponseConcept_Exclude( const CCommand &args ) +{ + // shouldn't use this extern elsewhere -- it's meant to be a hidden + // implementation detail + extern CRR_ConceptSymbolTable *g_pRRConceptTable; + Assert( g_pRRConceptTable ); + if ( !g_pRRConceptTable ) return; + + + // different things for different argument lengths + switch ( args.ArgC() ) + { + case 0: + { + AssertMsg( args.ArgC() > 0, "WTF error in ccommand parsing: zero arguments!\n" ); + return; + } + case 1: + { + // print usage info + Msg("Usage: rr_debugresponseconcept_exclude Concept1 Concept2 Concept3...\n"); + Msg("\tseparate multiple concepts with spaces.\n"); + Msg("\tcall with no arguments to see this message and a list of current excludes.\n"); + Msg("\tto reset the exclude list, type \"rr_debugresponseconcept_exclude !\"\n"); + + // print current excludes + Msg("\nCurrent exclude list:\n"); + if ( !CResponseSystem::m_DebugExcludeList.IsValidIndex( CResponseSystem::m_DebugExcludeList.Head() ) ) + { + Msg("\t\n"); + } + else + { + CResponseSystem::ExcludeList_t::IndexLocalType_t i; + for ( i = CResponseSystem::m_DebugExcludeList.Head() ; + CResponseSystem::m_DebugExcludeList.IsValidIndex(i) ; + i = CResponseSystem::m_DebugExcludeList.Next(i) ) + { + Msg( "\t%s\n", CResponseSystem::m_DebugExcludeList[i].GetStringConcept() ); + } + } + return; + } + case 2: + // deal with the erase operator + if ( args[1][0] == '!' ) + { + CResponseSystem::m_DebugExcludeList.Purge(); + Msg( "Exclude list emptied.\n" ); + return; + } + // else, FALL THROUGH: + default: + // add each arg to the exclude list + for ( int i = 1 ; i < args.ArgC() ; ++i ) + { + if ( !g_pRRConceptTable->Find(args[i]).IsValid() ) + { + Msg( "\t'%s' is not a known concept (adding it anyway)\n", args[i] ); + } + CRR_Concept concept( args[i] ); + CResponseSystem::m_DebugExcludeList.AddToTail( concept ); + } + } +} +#if RR_DUMPHASHINFO_ENABLED +static void CC_RR_DumpHashInfo( const CCommand &args ) +{ + defaultresponsesytem.m_InstancedSystems[0]->m_RulePartitions.PrintBucketInfo( defaultresponsesytem.m_InstancedSystems[0] ); +} +static ConCommand rr_dumphashinfo( "rr_dumphashinfo", CC_RR_DumpHashInfo, "Statistics on primary hash bucketing of response rule partitions"); + +void ResponseRulePartition::PrintBucketInfo( CResponseSystem *pSys ) +{ + struct bucktuple_t + { + int nBucket; + int nCount; + bucktuple_t() : nBucket(-1), nCount(-1) {}; + bucktuple_t( int bucket, int count ) : nBucket(bucket), nCount(count) {}; + + static int __cdecl SortCompare( const bucktuple_t * a, const bucktuple_t * b ) + { + return a->nCount - b->nCount; + } + }; + + CUtlVector infos( N_RESPONSE_PARTITIONS, N_RESPONSE_PARTITIONS ); + + float nAverage = 0; + for ( int i = 0 ; i < N_RESPONSE_PARTITIONS ; ++i ) + { + int count = m_RuleParts[i].Count(); + infos.AddToTail( bucktuple_t( i, count ) ); + nAverage += count; + } + nAverage /= N_RESPONSE_PARTITIONS; + infos.Sort( bucktuple_t::SortCompare ); + Msg( "%d buckets, %d total, %.2f average size\n", N_RESPONSE_PARTITIONS, Count(), nAverage ); + Msg( "8 shortest buckets:\n" ); + for ( int i = 0 ; i < 8 ; ++i ) + { + Msg("\t%d: %d\n", infos[i].nBucket, infos[i].nCount ); + } + Msg( "8 longest buckets:\n" ); + for ( int i = infos.Count() - 1 ; i >= infos.Count() - 9 ; --i ) + { + Msg("\t%d: %d\n", infos[i].nBucket, infos[i].nCount ); + } + int nempty = 0; + for ( nempty = 0 ; nempty < infos.Count() ; ++nempty ) + { + if ( infos[nempty].nCount != 0 ) + break; + } + Msg( "%d empty buckets\n", nempty ); + + /* + Msg( " Contents of longest bucket\nwho\tconcept\n" ); + tRuleDict &bucket = m_RuleParts[infos[infos.Count()-1].nBucket]; + for ( tRuleDict::IndexType_t i = bucket.FirstInorder(); bucket.IsValidIndex(i); i = bucket.NextInorder(i) ) + { + Rule &rule = bucket.Element(i) ; + Msg("%s\t%s\n", rule.GetValueForRuleCriterionByName( pSys, "who" ), rule.GetValueForRuleCriterionByName( pSys, CriteriaSet::ComputeCriteriaSymbol("concept") ) ); + } + */ +} +#endif \ No newline at end of file diff --git a/src/responserules/runtime/response_system.h b/src/responserules/runtime/response_system.h new file mode 100644 index 000000000..8f25a1b2a --- /dev/null +++ b/src/responserules/runtime/response_system.h @@ -0,0 +1,297 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: The CResponseSystem class. Don't include this header; include the response_types +// into which it is transcluded. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef RESPONSE_SYSTEM_H +#define RESPONSE_SYSTEM_H +#ifdef _WIN32 +#pragma once +#endif + +#include "utldict.h" +#include "utllinkedlist.h" +#include "stringpool.h" + +namespace ResponseRules +{ + typedef ResponseParams AI_ResponseParams ; + #define AI_CriteriaSet ResponseRules::CriteriaSet + + //----------------------------------------------------------------------------- + // Purpose: The database of all available responses. + // The Rules are partitioned based on a variety of factors (presently, + // speaker and concept) for faster lookup, basically a seperate-chained hash. + //----------------------------------------------------------------------------- + class CResponseSystem : public IResponseSystem + { + public: + CResponseSystem(); + ~CResponseSystem(); + + typedef void (CResponseSystem::*pfnResponseDispatch)( void ); + typedef void (CResponseSystem::*pfnParseRuleDispatch)( Rule & ); + typedef void (CResponseSystem::*pfnParseResponseDispatch)( ParserResponse &, ResponseGroup&, AI_ResponseParams * ); + typedef void (CResponseSystem::*pfnParseResponseGroupDispatch) ( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + + typedef CUtlMap< unsigned,pfnResponseDispatch > DispatchMap_t; + typedef CUtlMap< unsigned,pfnParseRuleDispatch > ParseRuleDispatchMap_t; + typedef CUtlMap< unsigned,pfnParseResponseDispatch > ParseResponseDispatchMap_t; + typedef CUtlMap< unsigned,pfnParseResponseGroupDispatch > ParseResponseGroupDispatchMap_t; + +#pragma region IResponseSystem + // IResponseSystem + virtual bool FindBestResponse( const CriteriaSet& set, CRR_Response& response, IResponseFilter *pFilter = NULL ); + virtual void GetAllResponses( CUtlVector *pResponses ); +#pragma endregion Implement interface from IResponseSystem + + virtual void Release() = 0; + + virtual void DumpRules(); + + bool IsCustomManagable() { return m_bCustomManagable; } + + void Clear(); + + void DumpDictionary( const char *pszName ); + + protected: + + void BuildDispatchTables(); + bool Dispatch( char const *pToken, unsigned int uiHash, DispatchMap_t &rMap ); + bool DispatchParseRule( char const *pToken, unsigned int uiHash, ParseRuleDispatchMap_t &rMap, Rule &newRule ); + bool DispatchParseResponse( char const *pToken, unsigned int uiHash, ParseResponseDispatchMap_t &rMap, ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + bool DispatchParseResponseGroup( char const *pToken, unsigned int uiHash, ParseResponseGroupDispatchMap_t &rMap, char const *responseGroupName, ResponseGroup& newGroup, AI_ResponseParams &groupResponseParams ); + + virtual const char *GetScriptFile( void ) = 0; + void LoadRuleSet( const char *setname ); + + void ResetResponseGroups(); + + float LookForCriteria( const CriteriaSet &criteriaSet, int iCriteria ); + float RecursiveLookForCriteria( const CriteriaSet &criteriaSet, Criteria *pParent ); + + public: + + void CopyRuleFrom( Rule *pSrcRule, ResponseRulePartition::tIndex iRule, CResponseSystem *pCustomSystem ); + void CopyCriteriaFrom( Rule *pSrcRule, Rule *pDstRule, CResponseSystem *pCustomSystem ); + void CopyResponsesFrom( Rule *pSrcRule, Rule *pDstRule, CResponseSystem *pCustomSystem ); + void CopyEnumerationsFrom( CResponseSystem *pCustomSystem ); + + //private: + + struct Enumeration + { + float value; + }; + + struct ResponseSearchResult + { + ResponseSearchResult() + { + group = NULL; + action = NULL; + } + + ResponseGroup *group; + ParserResponse *action; + }; + + inline bool ParseToken( void ) + { + if ( m_bUnget ) + { + m_bUnget = false; + return true; + } + if ( m_ScriptStack.Count() <= 0 ) + { + Assert( 0 ); + return false; + } + + m_ScriptStack[ 0 ].currenttoken = IEngineEmulator::Get()->ParseFile( m_ScriptStack[ 0 ].currenttoken, token, sizeof( token ) ); + m_ScriptStack[ 0 ].tokencount++; + return m_ScriptStack[ 0 ].currenttoken != NULL ? true : false; + } + + inline void Unget() + { + m_bUnget = true; + } + + inline bool TokenWaiting( void ) + { + if ( m_ScriptStack.Count() <= 0 ) + { + Assert( 0 ); + return false; + } + + const char *p = m_ScriptStack[ 0 ].currenttoken; + + if ( !p ) + { + Error( "AI_ResponseSystem: Unxpected TokenWaiting() with NULL buffer in %s", (char * ) m_ScriptStack[ 0 ].name ); + return false; + } + + + while ( *p && *p!='\n') + { + // Special handler for // comment blocks + if ( *p == '/' && *(p+1) == '/' ) + return false; + + if ( !V_isspace( *p ) || isalnum( *p ) ) + return true; + + p++; + } + + return false; + } + + void ParseOneResponse( const char *responseGroupName, ResponseGroup& group, ResponseParams *defaultParams = NULL ); + + void ParseInclude( void ); + void ParseResponse( void ); + void ParseCriterion( void ); + void ParseRule( void ); + void ParseEnumeration( void ); + + private: + void ParseRule_MatchOnce( Rule &newRule ); + void ParseRule_ApplyContextToWorld( Rule &newRule ); + void ParseRule_ApplyContextToSelf(Rule& newRule); + void ParseRule_ApplyContextToSquad(Rule& newRule); + void ParseRule_ApplyContextToEnemy(Rule& newRule); + void ParseRule_ApplyContext( Rule &newRule ); + void ParseRule_Response( Rule &newRule ); + //void ParseRule_ForceWeight( Rule &newRule ); + void ParseRule_Criteria( Rule &newRule ); + char const *m_pParseRuleName; + bool m_bParseRuleValid; + + void ParseResponse_Weight( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_PreDelay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_NoDelay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_DefaultDelay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_Delay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_SpeakOnce( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_NoScene( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_StopOnNonIdle( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_Odds( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_RespeakDelay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_WeaponDelay( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_Soundlevel( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_DisplayFirst( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_DisplayLast( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_Fire( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + void ParseResponse_Then( ParserResponse &newResponse, ResponseGroup& group, AI_ResponseParams *rp ); + + void ParseResponseGroup_Start( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + void ParseResponseGroup_PreDelay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + void ParseResponseGroup_NoDelay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + void ParseResponseGroup_DefaultDelay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + void ParseResponseGroup_Delay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + void ParseResponseGroup_SpeakOnce( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + void ParseResponseGroup_NoScene( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + void ParseResponseGroup_StopOnNonIdle( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + void ParseResponseGroup_Odds( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + void ParseResponseGroup_RespeakDelay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + void ParseResponseGroup_WeaponDelay( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + void ParseResponseGroup_Soundlevel( char const *responseGroupName, ResponseGroup &newGroup, AI_ResponseParams &groupResponseParams ); + +public: + int ParseOneCriterion( const char *criterionName ); + + bool Compare( const char *setValue, Criteria *c, bool verbose = false ); + bool CompareUsingMatcher( const char *setValue, Matcher& m, bool verbose = false ); + void ComputeMatcher( Criteria *c, Matcher& matcher ); + void ResolveToken( Matcher& matcher, char *token, size_t bufsize, char const *rawtoken ); + float LookupEnumeration( const char *name, bool& found ); + + ResponseRulePartition::tIndex FindBestMatchingRule( const CriteriaSet& set, bool verbose, float &scoreOfBestMatchingRule ); + + float ScoreCriteriaAgainstRule( const CriteriaSet& set, ResponseRulePartition::tRuleDict &dict, int irule, bool verbose = false ); + float RecursiveScoreSubcriteriaAgainstRule( const CriteriaSet& set, Criteria *parent, bool& exclude, bool verbose /*=false*/ ); + float ScoreCriteriaAgainstRuleCriteria( const CriteriaSet& set, int icriterion, bool& exclude, bool verbose = false ); + void FakeDepletes( ResponseGroup *g, IResponseFilter *pFilter ); + void RevertFakedDepletes( ResponseGroup *g ); + bool GetBestResponse( ResponseSearchResult& result, Rule *rule, bool verbose = false, IResponseFilter *pFilter = NULL ); + bool ResolveResponse( ResponseSearchResult& result, int depth, const char *name, bool verbose = false, IResponseFilter *pFilter = NULL ); + int SelectWeightedResponseFromResponseGroup( ResponseGroup *g, IResponseFilter *pFilter ); + void DescribeResponseGroup( ResponseGroup *group, int selected, int depth ); + void DebugPrint( int depth, const char *fmt, ... ); + + void LoadFromBuffer( const char *scriptfile, const char *buffer ); + + void GetCurrentScript( char *buf, size_t buflen ); + int GetCurrentToken() const; + void SetCurrentScript( const char *script ); + + inline bool IsRootCommand( unsigned int hash ) const + { + int slot = m_RootCommandHashes.Find( hash ); + return slot != m_RootCommandHashes.InvalidIndex(); + } + + inline bool IsRootCommand() const + { + return IsRootCommand( RR_HASH( token ) ); + } + + void PushScript( const char *scriptfile, unsigned char *buffer ); + void PopScript(void); + + void ResponseWarning( const char *fmt, ... ); + + CUtlDict< ResponseGroup, short > m_Responses; + CUtlDict< Criteria, short > m_Criteria; + // CUtlDict< Rule, short > m_Rules; + ResponseRulePartition m_RulePartitions; + CUtlDict< Enumeration, short > m_Enumerations; + + CUtlVector m_FakedDepletes; + + char token[ 1204 ]; + + bool m_bUnget; + + bool m_bCustomManagable; + + struct ScriptEntry + { + unsigned char *buffer; + FileNameHandle_t name; + const char *currenttoken; + int tokencount; + }; + + CUtlVector< ScriptEntry > m_ScriptStack; + CStringPool m_IncludedFiles; + + DispatchMap_t m_FileDispatch; + ParseRuleDispatchMap_t m_RuleDispatch; + ParseResponseDispatchMap_t m_ResponseDispatch; + ParseResponseGroupDispatchMap_t m_ResponseGroupDispatch; + CUtlRBTree< unsigned int > m_RootCommandHashes; + + // for debugging purposes only: concepts to be emitted from rr_debugresponses 2 + typedef CUtlLinkedList< CRR_Concept, unsigned short, false, unsigned int > ExcludeList_t; + static ExcludeList_t m_DebugExcludeList; + + friend class CDefaultResponseSystemSaveRestoreBlockHandler; + friend class CResponseSystemSaveRestoreOps; + }; + + // Some globals inherited from AI_Speech.h: + const float AIS_DEF_MIN_DELAY = 2.8; // Minimum amount of time an NPCs will wait after someone has spoken before considering speaking again + const float AIS_DEF_MAX_DELAY = 3.2; // Maximum amount of time an NPCs will wait after someone has spoken before considering speaking again +} + +#endif // RESPONSE_SYSTEM_H \ No newline at end of file diff --git a/src/responserules/runtime/response_types.cpp b/src/responserules/runtime/response_types.cpp new file mode 100644 index 000000000..30502f118 --- /dev/null +++ b/src/responserules/runtime/response_types.cpp @@ -0,0 +1,267 @@ +//========= Copyright © 1996-2010, Valve Corporation, All rights reserved. ============// +// +// Purpose: Core types for the response rules -- criteria, responses, rules, and matchers. +// +// $NoKeywords: $ +//=============================================================================// + +#include "rrbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +using namespace ResponseRules; + + +// bizarre function handed down from the misty days of yore +// and the original response system. a lot of stuff uses it +// and I can't be arsed to replace everything with the c stdlib +// stuff +namespace ResponseRules +{ + extern const char *ResponseCopyString( const char *in ); +}; + + +//-------------------- MATCHER ---------------------------------------------- + +Matcher::Matcher() +{ + valid = false; + isnumeric = false; + notequal = false; + usemin = false; + minequals = false; + usemax = false; + maxequals = false; + isbit = false; + maxval = 0.0f; + minval = 0.0f; + + token = UTL_INVAL_SYMBOL; + rawtoken = UTL_INVAL_SYMBOL; +} + +void Matcher::Describe( void ) +{ + if ( !valid ) + { + DevMsg( " invalid!\n" ); + return; + } + char sz[ 128 ]; + + sz[ 0] = 0; + int minmaxcount = 0; + if ( usemin ) + { + Q_snprintf( sz, sizeof( sz ), ">%s%.3f", minequals ? "=" : "", minval ); + minmaxcount++; + } + if ( usemax ) + { + char sz2[ 128 ]; + Q_snprintf( sz2, sizeof( sz2 ), "<%s%.3f", maxequals ? "=" : "", maxval ); + + if ( minmaxcount > 0 ) + { + Q_strncat( sz, " and ", sizeof( sz ), COPY_ALL_CHARACTERS ); + } + Q_strncat( sz, sz2, sizeof( sz ), COPY_ALL_CHARACTERS ); + minmaxcount++; + } + + if ( minmaxcount >= 1 ) + { + DevMsg( " matcher: %s\n", sz ); + return; + } + + if (isbit) + { + DevMsg(" matcher: &%s%s\n", notequal ? "~" : "", GetToken()); + return; + } + + if ( notequal ) + { + DevMsg( " matcher: !=%s\n", GetToken() ); + return; + } + + DevMsg( " matcher: ==%s\n", GetToken() ); +} + +void Matcher::SetToken( char const *s ) +{ + token = g_RS.AddString( s ); +} + +void Matcher::SetRaw( char const *raw ) +{ + rawtoken = g_RS.AddString( raw ); +} + +char const *Matcher::GetToken() +{ + if ( token.IsValid() ) + { + return g_RS.String( token ); + } + return ""; +} + +char const *Matcher::GetRaw() +{ + if ( rawtoken.IsValid() ) + { + return g_RS.String( rawtoken ); + } + return ""; +} + +//-------------------- CRITERIA ---------------------------------------------- + +Criteria::Criteria() +{ + value = NULL; + weight.SetFloat( 1.0f ); + required = false; +} +Criteria::Criteria(const Criteria& src ) +{ + operator=( src ); +} + +Criteria::~Criteria() +{ + // do nothing because we don't own name and value anymore +} + +Criteria& Criteria::operator =(const Criteria& src ) +{ + if ( this == &src ) + return *this; + + nameSym = src.nameSym; + value = ResponseCopyString( src.value ); + weight = src.weight; + required = src.required; + + matcher = src.matcher; + + int c = src.subcriteria.Count(); + subcriteria.EnsureCapacity( c ); + for ( int i = 0; i < c; i++ ) + { + subcriteria.AddToTail( src.subcriteria[ i ] ); + } + + return *this; +} + + +//-------------------- RESPONSE ---------------------------------------------- + + + +ParserResponse::ParserResponse() : m_followup() +{ + type = RESPONSE_NONE; + value = NULL; + weight.SetFloat( 1.0f ); + depletioncount = 0; + first = false; + last = false; +} + +ParserResponse& ParserResponse::operator =( const ParserResponse& src ) +{ + if ( this == &src ) + return *this; + weight = src.weight; + type = src.type; + value = ResponseCopyString( src.value ); + depletioncount = src.depletioncount; + first = src.first; + last = src.last; + params = src.params; + + m_followup.followup_concept = ResponseCopyString(src.m_followup.followup_concept); + m_followup.followup_contexts = ResponseCopyString(src.m_followup.followup_contexts); + m_followup.followup_target = ResponseCopyString(src.m_followup.followup_target); + m_followup.followup_entityioinput = ResponseCopyString(src.m_followup.followup_entityioinput); + m_followup.followup_entityiotarget = ResponseCopyString(src.m_followup.followup_entityiotarget); + m_followup.followup_delay = src.m_followup.followup_delay; + m_followup.followup_entityiodelay = src.m_followup.followup_entityiodelay; + + return *this; +} + +ParserResponse::ParserResponse( const ParserResponse& src ) +{ + operator=( src ); +} + +ParserResponse::~ParserResponse() +{ + // nothing to do, since we don't own + // the strings anymore +} + +// ------------ RULE --------------- + +Rule::Rule() : m_nForceWeight(0) +{ + m_bMatchOnce = false; + m_bEnabled = true; + m_szContext = NULL; + m_iContextFlags = 0; +} + +Rule& Rule::operator =( const Rule& src ) +{ + if ( this == &src ) + return *this; + + int i; + int c; + + c = src.m_Criteria.Count(); + m_Criteria.EnsureCapacity( c ); + for ( i = 0; i < c; i++ ) + { + m_Criteria.AddToTail( src.m_Criteria[ i ] ); + } + + c = src.m_Responses.Count(); + m_Responses.EnsureCapacity( c ); + for ( i = 0; i < c; i++ ) + { + m_Responses.AddToTail( src.m_Responses[ i ] ); + } + + SetContext( src.m_szContext ); + m_bMatchOnce = src.m_bMatchOnce; + m_bEnabled = src.m_bEnabled; + m_iContextFlags = src.m_iContextFlags; + m_nForceWeight = src.m_nForceWeight; + return *this; +} + +Rule::Rule( const Rule& src ) +{ + operator=(src); +} + +Rule::~Rule() +{ +} + +void Rule::SetContext( const char *context ) +{ + // we don't own the data we point to, so just update pointer + m_szContext = ResponseCopyString( context ); +} + + diff --git a/src/responserules/runtime/response_types_internal.cpp b/src/responserules/runtime/response_types_internal.cpp new file mode 100644 index 000000000..2d11bdd2c --- /dev/null +++ b/src/responserules/runtime/response_types_internal.cpp @@ -0,0 +1,120 @@ +//========= Copyright © 1996-2010, Valve Corporation, All rights reserved. ============// +// +// Purpose: Core types for the response rules -- criteria, responses, rules, and matchers. +// +// $NoKeywords: $ +//=============================================================================// + +#include "rrbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +using namespace ResponseRules; + + + + +ResponseRulePartition::ResponseRulePartition() +{ + Assert(true); + COMPILE_TIME_ASSERT( kIDX_ELEM_MASK < (1 << 16) ); + COMPILE_TIME_ASSERT( (kIDX_ELEM_MASK & (kIDX_ELEM_MASK + 1)) == 0 ); /// assert is power of two minus one +} + +ResponseRulePartition::~ResponseRulePartition() +{ + RemoveAll(); +} + +ResponseRulePartition::tIndex ResponseRulePartition::IndexFromDictElem( tRuleDict* pDict, int elem ) +{ + Assert( pDict ); + // If this fails, you've tried to build an index for a rule that's not stored + // in this partition + Assert( pDict >= m_RuleParts && pDict < m_RuleParts + N_RESPONSE_PARTITIONS ); + AssertMsg1( elem <= kIDX_ELEM_MASK, "A rule dictionary has %d elements; this exceeds the 255 that can be packed into an index.\n", elem ); + + int bucket = pDict - m_RuleParts; + return ( bucket << 16 ) | ( elem & kIDX_ELEM_MASK ); // this is a native op on PPC +} + + +char const *ResponseRulePartition::GetElementName( const tIndex &i ) const +{ + Assert( IsValid(i) ); + return m_RuleParts[ BucketFromIdx(i) ].GetElementName( PartFromIdx(i) ); +} + + +int ResponseRulePartition::Count( void ) +{ + int count = 0 ; + for ( int bukkit = 0 ; bukkit < N_RESPONSE_PARTITIONS ; ++bukkit ) + { + count += m_RuleParts[bukkit].Count(); + } + + return count; +} + +void ResponseRulePartition::RemoveAll( void ) +{ + for ( int bukkit = 0 ; bukkit < N_RESPONSE_PARTITIONS ; ++bukkit ) + { + for ( int i = m_RuleParts[bukkit].FirstInorder(); i != m_RuleParts[bukkit].InvalidIndex(); i = m_RuleParts[bukkit].NextInorder( i ) ) + { + delete m_RuleParts[bukkit][ i ]; + } + m_RuleParts[bukkit].RemoveAll(); + } +} + +// don't bucket "subject" criteria that prefix with operators, since stripping all that out again would +// be a big pain, and the most important rules that need subjects are tlk_remarks anyway. +static inline bool CanBucketBySubject( const char * RESTRICT pszSubject ) +{ + return pszSubject && + ( ( pszSubject[0] >= 'A' && pszSubject[0] <= 'Z' ) || + ( pszSubject[0] >= 'a' && pszSubject[0] <= 'z' ) ); +} + +ResponseRulePartition::tRuleDict &ResponseRulePartition::GetDictForRule( CResponseSystem *pSystem, Rule *pRule ) +{ + const static CUtlSymbol kWHO = CriteriaSet::ComputeCriteriaSymbol("Who"); + const static CUtlSymbol kCONCEPT = CriteriaSet::ComputeCriteriaSymbol("Concept"); + const static CUtlSymbol kSUBJECT = CriteriaSet::ComputeCriteriaSymbol("Subject"); + + const char *pszSpeaker = pRule->GetValueForRuleCriterionByName( pSystem, kWHO ); + const char *pszConcept = pRule->GetValueForRuleCriterionByName( pSystem, kCONCEPT ); + const Criteria *pSubjCrit = pRule->GetPointerForRuleCriterionByName( pSystem, kSUBJECT ); + + return m_RuleParts[ + GetBucketForSpeakerAndConcept( pszSpeaker, pszConcept, + ( pSubjCrit && pSubjCrit->required && CanBucketBySubject(pSubjCrit->value) ) ? + pSubjCrit->value : + NULL ) + ]; +} + + +void ResponseRulePartition::GetDictsForCriteria( CUtlVectorFixed< ResponseRulePartition::tRuleDict *, 2 > *pResult, const CriteriaSet &criteria ) +{ + pResult->RemoveAll(); + pResult->EnsureCapacity( 2 ); + + // get the values for Who and Concept, which are what we bucket on + int speakerIdx = criteria.FindCriterionIndex( "Who" ); + const char *pszSpeaker = speakerIdx != -1 ? criteria.GetValue( speakerIdx ) : NULL ; + + int conceptIdx = criteria.FindCriterionIndex( "Concept" ); + const char *pszConcept = conceptIdx != -1 ? criteria.GetValue( conceptIdx ) : NULL ; + + int subjectIdx = criteria.FindCriterionIndex( "Subject" ); + const char *pszSubject = subjectIdx != -1 ? criteria.GetValue( subjectIdx ) : NULL ; + + pResult->AddToTail( &m_RuleParts[ GetBucketForSpeakerAndConcept(pszSpeaker, pszConcept, pszSubject) ] ); + // also try the rules not specifying subject + pResult->AddToTail( &m_RuleParts[ GetBucketForSpeakerAndConcept(pszSpeaker, pszConcept, NULL) ] ); + +} \ No newline at end of file diff --git a/src/responserules/runtime/response_types_internal.h b/src/responserules/runtime/response_types_internal.h new file mode 100644 index 000000000..1de53ff8b --- /dev/null +++ b/src/responserules/runtime/response_types_internal.h @@ -0,0 +1,542 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: Core types for the response rules -- criteria, responses, rules, and matchers. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef RESPONSE_TYPES_INTERNAL_H +#define RESPONSE_TYPES_INTERNAL_H +#ifdef _WIN32 +#pragma once +#endif + +#include "responserules/response_types.h" +#include "utldict.h" + + +namespace ResponseRules +{ + + inline unsigned FASTCALL HashStringConventional( const char *pszKey ) + { + unsigned hash = 0xAAAAAAAA; // Alternating 1's and 0's to maximize the effect of the later multiply and add + + for( ; *pszKey ; pszKey++ ) + { + hash = ( ( hash << 5 ) + hash ) + (uint8)(*pszKey); + } + + return hash; + } + + // Note: HashString causes collisions!!! +#define RR_HASH HashStringConventional + +#pragma pack(push,1) + + class Matcher + { + public: + Matcher(); + + void Describe( void ); + + float maxval; + float minval; + + bool valid : 1; //1 + bool isnumeric : 1; //2 + bool notequal : 1; //3 + bool usemin : 1; //4 + bool minequals : 1; //5 + bool usemax : 1; //6 + bool maxequals : 1; //7 + bool isbit : 1; //8 + + void SetToken( char const *s ); + + char const *GetToken(); + + void SetRaw( char const *raw ); + + char const *GetRaw(); + + private: + CUtlSymbol token; + CUtlSymbol rawtoken; + }; +#pragma pack(pop) + + struct Criteria + { + Criteria(); + Criteria& operator =(const Criteria& src ); + + Criteria(const Criteria& src ); + ~Criteria(); + + // Does this criterion recursively contain more criteria? + inline bool IsSubCriteriaType() const + { + return ( subcriteria.Count() > 0 ) ? true : false; + } + + // const char *name; + CUtlSymbol nameSym; + const char *value; + float16 weight; + bool required; + + Matcher matcher; + + // Indices into sub criteria + CUtlVectorConservative< unsigned short > subcriteria; + }; + +#pragma pack(push,1) + /// This is a response block as read from the file, + /// different from CRR_Response which is what is handed + /// back to queries. + struct ParserResponse + { + DECLARE_SIMPLE_DATADESC_INSIDE_NAMESPACE(); + + ParserResponse(); + ParserResponse( const ParserResponse& src ); + ParserResponse& operator =( const ParserResponse& src ); + ~ParserResponse(); + + ResponseType_t GetType() { return (ResponseType_t)type; } + + ResponseParams params; + + const char *value; // fixed up value spot // 4 + float16 weight; // 6 + + byte depletioncount; // 7 + byte type : 6; // 8 + byte first : 1; // + byte last : 1; // + + ALIGN32 AI_ResponseFollowup m_followup; // info on whether I should force the other guy to say something + }; +#pragma pack(pop) + +#pragma pack(push,1) + struct ResponseGroup + { + DECLARE_SIMPLE_DATADESC_INSIDE_NAMESPACE(); + + ResponseGroup() + { + // By default visit all nodes before repeating + m_bSequential = false; + m_bNoRepeat = false; + m_bEnabled = true; + m_nCurrentIndex = 0; + m_bDepleteBeforeRepeat = true; + m_nDepletionCount = 1; + m_bHasFirst = false; + m_bHasLast = false; + } + + ResponseGroup( const ResponseGroup& src ) + { + int c = src.group.Count(); + for ( int i = 0; i < c; i++ ) + { + group.AddToTail( src.group[ i ] ); + } + + m_bDepleteBeforeRepeat = src.m_bDepleteBeforeRepeat; + m_nDepletionCount = src.m_nDepletionCount; + m_bHasFirst = src.m_bHasFirst; + m_bHasLast = src.m_bHasLast; + m_bSequential = src.m_bSequential; + m_bNoRepeat = src.m_bNoRepeat; + m_bEnabled = src.m_bEnabled; + m_nCurrentIndex = src.m_nCurrentIndex; + } + + ResponseGroup& operator=( const ResponseGroup& src ) + { + if ( this == &src ) + return *this; + int c = src.group.Count(); + for ( int i = 0; i < c; i++ ) + { + group.AddToTail( src.group[ i ] ); + } + + m_bDepleteBeforeRepeat = src.m_bDepleteBeforeRepeat; + m_nDepletionCount = src.m_nDepletionCount; + m_bHasFirst = src.m_bHasFirst; + m_bHasLast = src.m_bHasLast; + m_bSequential = src.m_bSequential; + m_bNoRepeat = src.m_bNoRepeat; + m_bEnabled = src.m_bEnabled; + m_nCurrentIndex = src.m_nCurrentIndex; + return *this; + } + + bool HasUndepletedChoices() const + { + if ( !m_bDepleteBeforeRepeat ) + return true; + + int c = group.Count(); + for ( int i = 0; i < c; i++ ) + { + if ( group[ i ].depletioncount != m_nDepletionCount ) + return true; + } + + return false; + } + + void MarkResponseUsed( int idx ) + { + if ( !m_bDepleteBeforeRepeat ) + return; + + if ( idx < 0 || idx >= group.Count() ) + { + Assert( 0 ); + return; + } + + group[ idx ].depletioncount = m_nDepletionCount; + } + + void ResetDepletionCount() + { + if ( !m_bDepleteBeforeRepeat ) + return; + ++m_nDepletionCount; + } + + void Reset() + { + ResetDepletionCount(); + SetEnabled( true ); + SetCurrentIndex( 0 ); + m_nDepletionCount = 1; + + for ( int i = 0; i < group.Count(); ++i ) + { + group[ i ].depletioncount = 0; + } + } + + bool HasUndepletedFirst( int& index ) + { + index = -1; + + if ( !m_bDepleteBeforeRepeat ) + return false; + + int c = group.Count(); + for ( int i = 0; i < c; i++ ) + { + ParserResponse *r = &group[ i ]; + + if ( ( r->depletioncount != m_nDepletionCount ) && r->first ) + { + index = i; + return true; + } + } + + return false; + } + + bool HasUndepletedLast( int& index ) + { + index = -1; + + if ( !m_bDepleteBeforeRepeat ) + return false; + + int c = group.Count(); + for ( int i = 0; i < c; i++ ) + { + ParserResponse *r = &group[ i ]; + + if ( ( r->depletioncount != m_nDepletionCount ) && r->last ) + { + index = i; + return true; + } + } + + return false; + } + + bool ShouldCheckRepeats() const { return m_bDepleteBeforeRepeat; } + int GetDepletionCount() const { return m_nDepletionCount; } + + bool IsSequential() const { return m_bSequential; } + void SetSequential( bool seq ) { m_bSequential = seq; } + + bool IsNoRepeat() const { return m_bNoRepeat; } + void SetNoRepeat( bool norepeat ) { m_bNoRepeat = norepeat; } + + bool IsEnabled() const { return m_bEnabled; } + void SetEnabled( bool enabled ) { m_bEnabled = enabled; } + + int GetCurrentIndex() const { return m_nCurrentIndex; } + void SetCurrentIndex( byte idx ) { m_nCurrentIndex = idx; } + + CUtlVector< ParserResponse > group; + + bool m_bEnabled; + + byte m_nCurrentIndex; + // Invalidation counter + byte m_nDepletionCount; + + // Use all slots before repeating any + bool m_bDepleteBeforeRepeat : 1; + bool m_bHasFirst : 1; + bool m_bHasLast : 1; + bool m_bSequential : 1; + bool m_bNoRepeat : 1; + }; +#pragma pack(pop) + +#pragma pack(push,1) + struct Rule + { + Rule(); + Rule( const Rule& src ); + ~Rule(); + Rule& operator =( const Rule& src ); + + void SetContext( const char *context ); + + const char *GetContext( void ) const { return m_szContext; } + + inline bool IsEnabled() const { return m_bEnabled; } + inline void Disable() { m_bEnabled = false; } + inline bool IsMatchOnce() const { return m_bMatchOnce; } + inline bool IsApplyContextToWorld() const { return (m_iContextFlags & APPLYCONTEXT_WORLD) != 0; } + + const char *GetValueForRuleCriterionByName( CResponseSystem *pSystem, const CUtlSymbol &pCritNameSym ); + const Criteria *GetPointerForRuleCriterionByName( CResponseSystem *pSystem, const CUtlSymbol &pCritNameSym ); + + // Indices into underlying criteria and response dictionaries + CUtlVectorConservative< unsigned short > m_Criteria; + CUtlVectorConservative< unsigned short> m_Responses; + + const char *m_szContext; + uint8 m_nForceWeight; + + int m_iContextFlags; + + bool m_bMatchOnce : 1; + bool m_bEnabled : 1; + + private: + // what is this, lisp? + const char *RecursiveGetValueForRuleCriterionByName( CResponseSystem *pSystem, const Criteria *pCrit, const CUtlSymbol &pCritNameSym ); + const Criteria *RecursiveGetPointerForRuleCriterionByName( CResponseSystem *pSystem, const Criteria *pCrit, const CUtlSymbol &pCritNameSym ); + }; +#pragma pack(pop) + + template + class CResponseDict : public CUtlMap + { + public: + CResponseDict() : CUtlMap( DefLessFunc( unsigned int ) ), m_ReverseMap( DefLessFunc( unsigned int ) ) + { + } + + I Insert( const char *pName, const T &element ) + { + char const *pString = ResponseCopyString( pName ); + unsigned int hash = RR_HASH( pString ); + m_ReverseMap.Insert( hash, pString ); + return CUtlMap::Insert( hash, element ); + } + + I Insert( const char *pName ) + { + char const *pString = ResponseCopyString( pName ); + unsigned int hash = RR_HASH( pString ); + m_ReverseMap.Insert( hash, pString ); + return CUtlMap::Insert( hash ); + } + + I Find( char const *pName ) const + { + unsigned int hash = RR_HASH( pName ); + return CUtlMap::Find( hash ); + } + + const char *GetElementName( I i ) + { + int k = Key( i ); + int slot = m_ReverseMap.Find( k ); + if ( slot == m_ReverseMap.InvalidIndex() ) + return ""; + return m_ReverseMap[ slot ]; + } + + const char *GetElementName( I i ) const + { + int k = Key( i ); + int slot = m_ReverseMap.Find( k ); + if ( slot == m_ReverseMap.InvalidIndex() ) + return ""; + return m_ReverseMap[ slot ]; + } + + private: + CUtlMap< unsigned int, const char * > m_ReverseMap; + + }; + + // define this to 1 to enable printing some occupancy + // information on the response system via concommmand + // rr_dumphashinfo + #define RR_DUMPHASHINFO_ENABLED 0 + // The Rules are partitioned based on a variety of factors (presently, + // speaker and concept) for faster lookup, basically a seperate-chained hash. + struct ResponseRulePartition + { + ResponseRulePartition( void ); + ~ResponseRulePartition(); + + typedef CResponseDict< Rule * > tRuleDict; + typedef uint32 tIndex; // an integer that can be used to find any rule in the dict + + /// get the appropriate m_rules dict for the provided rule + tRuleDict &GetDictForRule( CResponseSystem *pSystem, Rule *pRule ); + + /// get all bucket full of rules that might possibly match the given criteria. + /// (right now they are bucketed such that all rules that can possibly match a + /// criteria are in one of two dictionaries) + void GetDictsForCriteria( CUtlVectorFixed< ResponseRulePartition::tRuleDict *, 2 > *pResult, const CriteriaSet &criteria ); + + // dump everything. + void RemoveAll(); + + inline Rule &operator[]( tIndex idx ); + int Count( void ); // number of elements inside, but you can't iterate from 0 to this + char const *GetElementName( const tIndex &i ) const; + + /// given a dictionary and an element number inside that dict, + /// return a tIndex + tIndex IndexFromDictElem( tRuleDict* pDict, int elem ); + + // for iteration: + inline tIndex First( void ); + inline tIndex Next( const tIndex &idx ); + inline bool IsValid( const tIndex &idx ) const; + inline static tIndex InvalidIdx( void ) + { + return ((tIndex) -1); + } + + // used only for debug prints, do not rely on them otherwise + inline unsigned int BucketFromIdx( const tIndex &idx ) const ; + inline unsigned int PartFromIdx( const tIndex &idx ) const ; + + enum { + N_RESPONSE_PARTITIONS = 256, + kIDX_ELEM_MASK = 0xFFF, ///< this is used to mask the element number part of a ResponseRulePartition::tIndex + }; + +#if RR_DUMPHASHINFO_ENABLED + void PrintBucketInfo( CResponseSystem *pSys ); +#endif + + private: + tRuleDict m_RuleParts[N_RESPONSE_PARTITIONS]; + unsigned int GetBucketForSpeakerAndConcept( const char *pszSpeaker, const char *pszConcept, const char *pszSubject ); + }; + + // // // // // inline functions + + inline ResponseRulePartition::tIndex ResponseRulePartition::First( void ) + { + // find the first bucket that has anything + for ( int bucket = 0 ; bucket < N_RESPONSE_PARTITIONS; bucket++ ) + { + if ( m_RuleParts[bucket].Count() > 0 ) + return bucket << 16; + } + return InvalidIdx(); + } + + inline ResponseRulePartition::tIndex ResponseRulePartition::Next( const tIndex &idx ) + { + int bucket = BucketFromIdx( idx ); + unsigned int elem = PartFromIdx( idx ); + Assert( IsValid(idx) ); + AssertMsg( elem < kIDX_ELEM_MASK, "Too many response rules! Overflow! Doom!" ); + if ( elem + 1 < m_RuleParts[bucket].Count() ) + { + return idx+1; + } + else + { + // walk through the other buckets, skipping empty ones, until we find one with responses and give up. + while ( ++bucket < N_RESPONSE_PARTITIONS ) + { + if ( m_RuleParts[bucket].Count() > 0 ) + { + // 0th element in nth bucket + return bucket << 16; + } + } + + // out of buckets + return InvalidIdx(); + + } + } + + inline Rule &ResponseRulePartition::operator[]( tIndex idx ) + { + Assert( IsValid(idx) ); + return *m_RuleParts[ BucketFromIdx(idx) ][ PartFromIdx(idx) ] ; + } + + inline unsigned int ResponseRulePartition::BucketFromIdx( const tIndex &idx ) const + { + return idx >> 16; + } + + inline unsigned int ResponseRulePartition::PartFromIdx( const tIndex &idx ) const + { + return idx & kIDX_ELEM_MASK; + } + + inline bool ResponseRulePartition::IsValid( const tIndex & idx ) const + { + // make sure that the idx type for the dicts is still short + COMPILE_TIME_ASSERT( sizeof(m_RuleParts[0].FirstInorder()) == 2 ); + + if ( idx == -1 ) + return false; + + int bucket = idx >> 16; + unsigned int elem = idx & kIDX_ELEM_MASK; + + return ( bucket < N_RESPONSE_PARTITIONS && + elem < m_RuleParts[bucket].Count() ); + } + + //----------------------------------------------------------------------------- + // PARSER TYPES -- these are internal to the response system, and represent + // the objects as loaded from disk. + //----------------------------------------------------------------------------- + + +} + +#include "response_system.h" + +#endif \ No newline at end of file diff --git a/src/responserules/runtime/rr_convars.cpp b/src/responserules/runtime/rr_convars.cpp new file mode 100644 index 000000000..986ae0cd4 --- /dev/null +++ b/src/responserules/runtime/rr_convars.cpp @@ -0,0 +1,14 @@ +//========= Copyright © 1996-2010, Valve Corporation, All rights reserved. ============// +// +// Purpose: Convars used by the response rule system. +// +// $NoKeywords: $ +//=============================================================================// + +#include "rrbase.h" +#include + + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + diff --git a/src/responserules/runtime/rr_response.cpp b/src/responserules/runtime/rr_response.cpp new file mode 100644 index 000000000..6e9aae277 --- /dev/null +++ b/src/responserules/runtime/rr_response.cpp @@ -0,0 +1,330 @@ +#include "..\..\public\responserules\response_types.h" +#include "..\..\public\responserules\response_types.h" +//===== Copyright © 1996-2005, Valve Corporation, All rights reserved. ======// +// +// Purpose: +// +// $NoKeywords: $ +// +//===========================================================================// +#include "rrbase.h" + +#include + +/* +#include "AI_Criteria.h" +#include "ai_speech.h" +#include +#include "engine/IEngineSound.h" +*/ + +// memdbgon must be the last include file in a .cpp file!!! +#include + +using namespace ResponseRules; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CRR_Response::CRR_Response() : m_fMatchScore(0) +{ + m_Type = ResponseRules::RESPONSE_NONE; + m_szResponseName[0] = 0; + m_szMatchingRule[0]=0; + m_szContext = NULL; + m_iContextFlags = 0; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +CRR_Response::CRR_Response( const CRR_Response &from ) : m_fMatchScore(0) +{ + // Assert( (void*)(&m_Type) == (void*)this ); + Invalidate(); + memcpy( this, &from, sizeof(*this) ); + m_szContext = NULL; + SetContext( from.m_szContext ); + m_iContextFlags = from.m_iContextFlags; +} + + +//----------------------------------------------------------------------------- +CRR_Response &CRR_Response::operator=( const CRR_Response &from ) +{ + // Assert( (void*)(&m_Type) == (void*)this ); + Invalidate(); + memcpy( this, &from, sizeof(*this) ); + m_szContext = NULL; + SetContext( from.m_szContext ); + m_iContextFlags = from.m_iContextFlags; + return *this; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CRR_Response::~CRR_Response() +{ + if (m_szContext) + delete[] m_szContext; +} + +void CRR_Response::Invalidate() +{ + if (m_szContext) + { + delete[] m_szContext; + m_szContext = NULL; + } + m_Type = ResponseRules::RESPONSE_NONE; + m_szResponseName[0] = 0; + // not really necessary: + /* + m_szMatchingRule[0]=0; + m_szContext = NULL; + m_bApplyContextToWorld = false; + */ +} + +// please do not new or delete CRR_Responses. +void CRR_Response::operator delete(void* p) +{ + AssertMsg(false, "DO NOT new or delete CRR_Response s."); + free(p); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *response - +// *criteria - +//----------------------------------------------------------------------------- +void CRR_Response::Init( ResponseType_t type, const char *responseName, const ResponseParams& responseparams, const char *ruleName, const char *applyContext, int iContextFlags) +{ + m_Type = type; + Q_strncpy( m_szResponseName, responseName, sizeof( m_szResponseName ) ); + // Copy underlying criteria + Q_strncpy( m_szMatchingRule, ruleName ? ruleName : "NULL", sizeof( m_szMatchingRule ) ); + m_Params = responseparams; + SetContext( applyContext ); + m_iContextFlags = iContextFlags; +} + +//----------------------------------------------------------------------------- +// Purpose: Debug-print the response. You can optionally pass in the criteria +// used to come up with this response (usually present in the calling function) +// if you want to print that as well. DO NOT store the entire criteria set in +// CRR_Response just to make this debug print cleaner. +//----------------------------------------------------------------------------- +void CRR_Response::Describe( const CriteriaSet *pDebugCriteria ) +{ + if ( pDebugCriteria ) + { + DevMsg( "Search criteria:\n" ); + pDebugCriteria->Describe(); + } + + if ( m_szMatchingRule[ 0 ] ) + { + DevMsg( "Matched rule '%s', ", m_szMatchingRule ); + } + if ( m_szContext ) + { + DevMsg("Contexts to set '%s' on ", m_szContext); + if (m_iContextFlags & APPLYCONTEXT_WORLD) + DevMsg("world, "); + else if (m_iContextFlags & APPLYCONTEXT_SQUAD) + DevMsg("squad, "); + else if (m_iContextFlags & APPLYCONTEXT_ENEMY) + DevMsg("enemy, "); + else + DevMsg("speaker, "); + } + + DevMsg( "response %s = '%s'\n", DescribeResponse( (ResponseType_t)m_Type ), m_szResponseName ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : char const +//----------------------------------------------------------------------------- +void CRR_Response::GetName( char *buf, size_t buflen ) const +{ + Q_strncpy( buf, m_szResponseName, buflen ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : char const +//----------------------------------------------------------------------------- +void CRR_Response::GetResponse( char *buf, size_t buflen ) const +{ + GetName( buf, buflen ); +} + +const char* ResponseRules::CRR_Response::GetNamePtr() const +{ + return m_szResponseName; +} +const char* ResponseRules::CRR_Response::GetResponsePtr() const +{ + return m_szResponseName; +} +//----------------------------------------------------------------------------- +// Purpose: +// Input : type - +// Output : char const +//----------------------------------------------------------------------------- +const char *CRR_Response::DescribeResponse( ResponseType_t type ) +{ + if ( (int)type < 0 || (int)type >= ResponseRules::NUM_RESPONSES ) + { + Assert( 0 ); + return "???CRR_Response bogus index"; + } + + switch( type ) + { + default: + { + Assert( 0 ); + } + // Fall through + case ResponseRules::RESPONSE_NONE: + return "RESPONSE_NONE"; + case ResponseRules::RESPONSE_SPEAK: + return "RESPONSE_SPEAK"; + case ResponseRules::RESPONSE_SENTENCE: + return "RESPONSE_SENTENCE"; + case ResponseRules::RESPONSE_SCENE: + return "RESPONSE_SCENE"; + case ResponseRules::RESPONSE_RESPONSE: + return "RESPONSE_RESPONSE"; + case ResponseRules::RESPONSE_PRINT: + return "RESPONSE_PRINT"; + case ResponseRules::RESPONSE_ENTITYIO: + return "RESPONSE_ENTITYIO"; + } + + return "RESPONSE_NONE"; +} + +/* +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRR_Response::Release() +{ + delete this; +} +*/ + +//----------------------------------------------------------------------------- +// Purpose: +// Output : soundlevel_t +//----------------------------------------------------------------------------- +soundlevel_t CRR_Response::GetSoundLevel() const +{ + if ( m_Params.flags & ResponseParams::RG_SOUNDLEVEL ) + { + return (soundlevel_t)m_Params.soundlevel; + } + + return SNDLVL_TALKING; +} + +float CRR_Response::GetRespeakDelay( void ) const +{ + if ( m_Params.flags & ResponseParams::RG_RESPEAKDELAY ) + { + interval_t temp; + m_Params.respeakdelay.ToInterval( temp ); + return RandomInterval( temp ); + } + + return 0.0f; +} + +float CRR_Response::GetWeaponDelay( void ) const +{ + if ( m_Params.flags & ResponseParams::RG_WEAPONDELAY ) + { + interval_t temp; + m_Params.weapondelay.ToInterval( temp ); + return RandomInterval( temp ); + } + + return 0.0f; +} + +bool CRR_Response::GetSpeakOnce( void ) const +{ + if ( m_Params.flags & ResponseParams::RG_SPEAKONCE ) + { + return true; + } + + return false; +} + +bool CRR_Response::ShouldntUseScene( void ) const +{ + return ( m_Params.flags & ResponseParams::RG_DONT_USE_SCENE ) != 0; +} + +bool CRR_Response::ShouldBreakOnNonIdle( void ) const +{ + return ( m_Params.flags & ResponseParams::RG_STOP_ON_NONIDLE ) != 0; +} + +int CRR_Response::GetOdds( void ) const +{ + if ( m_Params.flags & ResponseParams::RG_ODDS ) + { + return m_Params.odds; + } + return 100; +} + +float CRR_Response::GetDelay() const +{ + if ( m_Params.flags & ResponseParams::RG_DELAYAFTERSPEAK ) + { + interval_t temp; + m_Params.delay.ToInterval( temp ); + return RandomInterval( temp ); + } + return 0.0f; +} + +float CRR_Response::GetPreDelay() const +{ + if ( m_Params.flags & ResponseParams::RG_DELAYBEFORESPEAK ) + { + interval_t temp; + m_Params.predelay.ToInterval( temp ); + return RandomInterval( temp ); + } + return 0.0f; +} + +//----------------------------------------------------------------------------- +// Purpose: Sets context string +// Output : void +//----------------------------------------------------------------------------- +void CRR_Response::SetContext( const char *context ) +{ + if (m_szContext) + { + delete[] m_szContext; + m_szContext = NULL; + } + + if ( context ) + { + int len = Q_strlen( context ); + m_szContext = new char[ len + 1 ]; + Q_memcpy( m_szContext, context, len ); + m_szContext[ len ] = 0; + } +} diff --git a/src/responserules/runtime/rr_speechconcept.cpp b/src/responserules/runtime/rr_speechconcept.cpp new file mode 100644 index 000000000..7e1e04abf --- /dev/null +++ b/src/responserules/runtime/rr_speechconcept.cpp @@ -0,0 +1,73 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "rrbase.h" + + +// memdbgon must be the last include file in a .cpp file!!! +#include + +#if RR_CONCEPTS_ARE_STRINGS +#pragma error("RR_CONCEPTS_ARE_STRINGS no longer supported") +#else + +using namespace ResponseRules; + +// Used to turn ad-hoc concept from strings into numbers. +CRR_ConceptSymbolTable *g_pRRConceptTable = NULL; + +// Q&D hack to defer initialization of concept table until I can figure out where it +// really needs to come from. +static void InitializeRRConceptTable() +{ + if (g_pRRConceptTable == NULL) + { + g_pRRConceptTable = new CRR_ConceptSymbolTable( 64, 64, true ); + } +} + +// construct from string +CRR_Concept::CRR_Concept(const char *fromString) +{ + InitializeRRConceptTable(); + m_iConcept = g_pRRConceptTable->AddString(fromString); +} + +CRR_Concept &CRR_Concept::operator=(const char *fromString) +{ + InitializeRRConceptTable(); + m_iConcept = g_pRRConceptTable->AddString(fromString); + return *this; +} + +bool CRR_Concept::operator==(const char *pszConcept) +{ + int otherConcept = g_pRRConceptTable->Find(pszConcept); + return ( otherConcept != UTL_INVAL_SYMBOL && otherConcept == m_iConcept ); +} + +const char *CRR_Concept::GetStringConcept() const +{ + InitializeRRConceptTable(); + AssertMsg( m_iConcept.IsValid(), "AI Concept has invalid string symbol.\n" ); + const char * retval = g_pRRConceptTable->String(m_iConcept); + AssertMsg( retval, "An RR_Concept couldn't find its string in the symbol table!\n" ); + if (retval == NULL) + { + Warning( "An RR_Concept couldn't find its string in the symbol table!\n" ); + retval = ""; + } + return retval; +} + +const char *CRR_Concept::GetStringForGenericId(tGenericId genericId) +{ + InitializeRRConceptTable(); + return g_pRRConceptTable->String(genericId); +} + +#endif diff --git a/src/responserules/runtime/rrbase.h b/src/responserules/runtime/rrbase.h new file mode 100644 index 000000000..e7af74dee --- /dev/null +++ b/src/responserules/runtime/rrbase.h @@ -0,0 +1,59 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef RRBASE_H +#define RRBASE_H +#ifdef _WIN32 +#pragma once +#endif + +#ifdef _WIN32 +// Silence certain warnings +// #pragma warning(disable : 4244) // int or float down-conversion +// #pragma warning(disable : 4305) // int or float data truncation +// #pragma warning(disable : 4201) // nameless struct/union +// #pragma warning(disable : 4511) // copy constructor could not be generated +// #pragma warning(disable : 4675) // resolved overload was found by argument dependent lookup +#endif + +#ifdef _DEBUG +#define DEBUG 1 +#endif + +// Misc C-runtime library headers +#include +#include +#include + +// tier 0 +#include "tier0/dbg.h" +#include "tier0/platform.h" +#include "basetypes.h" + +// tier 1 +#include "tier1/strtools.h" +#include "utlvector.h" +#include "utlsymbol.h" + +// tier 2 +#include "string_t.h" + +// Shared engine/DLL constants +#include "const.h" +#include "edict.h" + +// app +#if defined(_X360) +#define DISABLE_DEBUG_HISTORY 1 +#endif + +#include "responserules/response_types.h" +#include "response_types_internal.h" +#include "responserules/response_host_interface.h" + + +#endif // CBASE_H diff --git a/src/responserules/runtime/rrrlib.cpp b/src/responserules/runtime/rrrlib.cpp new file mode 100644 index 000000000..0d2c49fd7 --- /dev/null +++ b/src/responserules/runtime/rrrlib.cpp @@ -0,0 +1,13 @@ +/// PLACEHOLDER FILE FOR RESPONSE RULES RUNTIME LIBRARY + +#include "rrbase.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +namespace ResponseRules +{ + /// Custom symbol table for the response rules. + CUtlSymbolTable g_RS; +}; \ No newline at end of file diff --git a/src/responserules/runtime/stdafx.cpp b/src/responserules/runtime/stdafx.cpp new file mode 100644 index 000000000..4199359f2 --- /dev/null +++ b/src/responserules/runtime/stdafx.cpp @@ -0,0 +1,11 @@ +//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: Builds the precompiled header for the game DLL +// +// $NoKeywords: $ +//=============================================================================// + + +#include "rrbase.h" + +// NOTE: DO NOT ADD ANY CODE OR HEADERS TO THIS FILE!!! diff --git a/src/game/shared/interval.cpp b/src/tier1/interval.cpp similarity index 100% rename from src/game/shared/interval.cpp rename to src/tier1/interval.cpp diff --git a/src/tier1/tier1.vpc b/src/tier1/tier1.vpc index 6f163a8be..bb11105f4 100644 --- a/src/tier1/tier1.vpc +++ b/src/tier1/tier1.vpc @@ -39,6 +39,7 @@ $Project "tier1" $File "generichash.cpp" $File "ilocalize.cpp" $File "interface.cpp" + $File "interval.cpp" $File "KeyValues.cpp" $File "kvpacker.cpp" $File "lzmaDecoder.cpp" diff --git a/src/vpc_scripts/groups.vgc b/src/vpc_scripts/groups.vgc index 07be80d15..64b16a15f 100644 --- a/src/vpc_scripts/groups.vgc +++ b/src/vpc_scripts/groups.vgc @@ -23,6 +23,7 @@ $Group "game" "tier1" "vgui_controls" "fgdlib" + "responserules" } $Group "shaders" @@ -42,6 +43,7 @@ $Group "everything" "motionmapper" "phonemeextractor" "raytrace" + "responserules" "qc_eyes" "server" "serverplugin_empty" @@ -65,5 +67,7 @@ $Group "dedicated" "mathlib" "server" "tier1" + "fgdlib" + "responserules" } diff --git a/src/vpc_scripts/projects.vgc b/src/vpc_scripts/projects.vgc index 0d4da2ee5..d2b92d283 100644 --- a/src/vpc_scripts/projects.vgc +++ b/src/vpc_scripts/projects.vgc @@ -80,6 +80,11 @@ $Project "server" "game\server\server_lazuul.vpc" [($WIN32||$POSIX) && $LAZUUL] } +$Project "responserules" +{ + "responserules\runtime\response_rules.vpc" [$WINDOWS||$X360||$POSIX] +} + $Project "mathlib" { "mathlib\mathlib.vpc" [$WINDOWS||$X360||$POSIX]