From 309f8019dd301b733db46382b91f64401c629e49 Mon Sep 17 00:00:00 2001 From: "ALLEN-PC\\acj30" Date: Mon, 12 Aug 2024 22:31:22 -0500 Subject: [PATCH] Choreography-driven sentence system --- sp/src/game/server/ai_speech_new.cpp | 13 + sp/src/game/server/baseflex.cpp | 8 + sp/src/game/server/baseflex.h | 1 + sp/src/game/server/hl2/env_speaker.cpp | 13 + sp/src/game/server/mapbase/choreosentence.cpp | 985 ++++++++++++++++++ sp/src/game/server/mapbase/choreosentence.h | 67 ++ sp/src/game/server/sceneentity.cpp | 256 +++++ sp/src/game/server/sceneentity.h | 1 + sp/src/game/server/server_mapbase.vpc | 2 + sp/src/game/shared/mapbase/mapbase_shared.cpp | 9 +- sp/src/public/responserules/response_types.h | 1 + .../responserules/runtime/response_system.cpp | 2 + sp/src/responserules/runtime/rr_response.cpp | 2 + 13 files changed, 1357 insertions(+), 3 deletions(-) create mode 100644 sp/src/game/server/mapbase/choreosentence.cpp create mode 100644 sp/src/game/server/mapbase/choreosentence.h diff --git a/sp/src/game/server/ai_speech_new.cpp b/sp/src/game/server/ai_speech_new.cpp index d29b6b39f8..6010ceb6de 100644 --- a/sp/src/game/server/ai_speech_new.cpp +++ b/sp/src/game/server/ai_speech_new.cpp @@ -18,6 +18,7 @@ #include "sceneentity.h" #include "ai_speechqueue.h" #ifdef MAPBASE +#include "mapbase/choreosentence.h" #include "ai_squad.h" #endif @@ -846,6 +847,9 @@ bool CAI_Expresser::SpeakDispatchResponse( AIConcept_t concept, AI_Response *res case ResponseRules::RESPONSE_NONE: break; +#ifdef MAPBASE + case ResponseRules::RESPONSE_CHOREOSENTENCE: +#endif case ResponseRules::RESPONSE_SPEAK: { if ( !result->ShouldntUseScene() ) @@ -1194,6 +1198,15 @@ float CAI_Expresser::GetResponseDuration( AI_Response *result ) case ResponseRules::RESPONSE_NONE: case ResponseRules::RESPONSE_ENTITYIO: return 0.0f; +#ifdef MAPBASE + case ResponseRules::RESPONSE_CHOREOSENTENCE: + { + const ChoreoSentence_t *pSentence = LookupChoreoSentence( GetOuter(), response ); + if (pSentence) + return GetChoreoSentenceDuration( GetOuter(), *pSentence ); + } + break; +#endif } return 0.0f; diff --git a/sp/src/game/server/baseflex.cpp b/sp/src/game/server/baseflex.cpp index 49f3c189fe..f12173b398 100644 --- a/sp/src/game/server/baseflex.cpp +++ b/sp/src/game/server/baseflex.cpp @@ -2093,8 +2093,16 @@ float CBaseFlex::PlayScene( const char *pszScene, float flDelay, AI_Response *re #ifdef MAPBASE float CBaseFlex::PlayAutoGeneratedSoundScene( const char *soundname, float flDelay, AI_Response *response, IRecipientFilter *filter ) { + if (response && response->GetType() == ResponseRules::RESPONSE_CHOREOSENTENCE) + return InstancedChoreoSentenceScene( this, soundname, NULL, flDelay, false, response, false, filter ); + return InstancedAutoGeneratedSoundScene( this, soundname, NULL, flDelay, false, response, false, filter ); } + +float CBaseFlex::PlayChoreoSentenceScene( const char *pszSentence, float flDelay, AI_Response *response, IRecipientFilter *filter ) +{ + return InstancedChoreoSentenceScene( this, pszSentence, NULL, flDelay, false, response, false, filter ); +} #else float CBaseFlex::PlayAutoGeneratedSoundScene( const char *soundname ) { diff --git a/sp/src/game/server/baseflex.h b/sp/src/game/server/baseflex.h index ec809534af..186d0761bb 100644 --- a/sp/src/game/server/baseflex.h +++ b/sp/src/game/server/baseflex.h @@ -129,6 +129,7 @@ class CBaseFlex : public CBaseAnimatingOverlay virtual float PlayScene( const char *pszScene, float flDelay = 0.0f, AI_Response *response = NULL, IRecipientFilter *filter = NULL ); #ifdef MAPBASE + virtual float PlayChoreoSentenceScene( const char *pszSentenceName, float flDelay = 0.0f, AI_Response *response = NULL, IRecipientFilter *filter = NULL ); virtual float PlayAutoGeneratedSoundScene( const char *soundname, float flDelay = 0.0f, AI_Response *response = NULL, IRecipientFilter *filter = NULL ); #else virtual float PlayAutoGeneratedSoundScene( const char *soundname ); diff --git a/sp/src/game/server/hl2/env_speaker.cpp b/sp/src/game/server/hl2/env_speaker.cpp index ad46a25c85..6413023f7a 100644 --- a/sp/src/game/server/hl2/env_speaker.cpp +++ b/sp/src/game/server/hl2/env_speaker.cpp @@ -297,6 +297,19 @@ void CSpeaker::DispatchResponse( const char *conceptName ) CAI_Expresser::RunScriptResponse( pTarget, response, &set, true ); break; } +#endif +#ifdef MAPBASE + case ResponseRules::RESPONSE_CHOREOSENTENCE: + { + CBaseFlex *pFlex = NULL; + if (pTarget != this) + { + // Attempt to get flex on the target + pFlex = dynamic_cast(pTarget); + } + InstancedAutoGeneratedSoundScene(pFlex, response); + break; + } #endif default: break; diff --git a/sp/src/game/server/mapbase/choreosentence.cpp b/sp/src/game/server/mapbase/choreosentence.cpp new file mode 100644 index 0000000000..9edca7e711 --- /dev/null +++ b/sp/src/game/server/mapbase/choreosentence.cpp @@ -0,0 +1,985 @@ +//========= Mapbase - https://github.com/mapbase-source/source-sdk-2013 ============// +// +// Purpose: VCD-based sentences. +// +//=============================================================================// + +#include "cbase.h" +#include "utlhashtable.h" +#include "engine/IEngineSound.h" +#include "soundchars.h" +#include "choreosentence.h" +#include "mapbase/matchers.h" +#include "ai_speech_new.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//---------------------------------------------------------------------------- + +#define IsVirtualWord(pszWord) (pszWord[0] == 'V' && pszWord[1] == '_') +#define RemoveVirtualPrefix(pszWord) IsVirtualWord(pszWord) ? pszWord + 2 : pszWord + +//---------------------------------------------------------------------------- + +class CChoreoSentenceSystem : public CAutoGameSystem +{ +public: + + struct VirtualWord_t; + struct VirtualWordDefinition_t; + struct VirtualWordDefinitionList_t; + struct SentenceContextPrerequisite_t; + + //---------------------------------------------------------------------------- + + CChoreoSentenceSystem() : m_Strings( 256, 0, &StringLessThan ) { } + ~CChoreoSentenceSystem() { PurgeSentences(); } + + bool Init() + { + // Load the default choreo sentences files + LoadChoreoSentenceManifest( "scripts/choreosentences_manifest.txt" ); + return true; + } + + void Shutdown() + { + PurgeSentences(); + } + + //---------------------------------------------------------------------------- + + void LoadChoreoSentenceManifest( const char *pszFile ) + { + KeyValues *pManifest = new KeyValues( "ChoreoSentencesManifest" ); + if (pManifest->LoadFromFile( filesystem, pszFile )) + { + FOR_EACH_SUBKEY( pManifest, pSubKey ) + { + LoadChoreoSentenceFile( pSubKey->GetString() ); + } + } + pManifest->deleteThis(); + + //m_Strings.Compact( true ); + } + + void LoadChoreoSentenceFile( const char *pszFile ) + { + KeyValues *pManifest = new KeyValues( "ChoreoSentences" ); + if (pManifest->LoadFromFile( filesystem, pszFile )) + { + //---------------------------------------------------------------------------- + // Context Prerequisites + //---------------------------------------------------------------------------- + KeyValues *pSentenceContextPrerequisites = pManifest->FindKey( "SentenceContextPrerequisites" ); + FOR_EACH_SUBKEY( pSentenceContextPrerequisites, pSubKey ) + { + KeyValues *pCondition = pSubKey->GetFirstSubKey(); + if (pCondition) + { + // + // Only add if there is a subkey for condition + // + int i = m_ContextPrerequisites.AddToTail(); + m_ContextPrerequisites[i].nConditionType = DeduceCondition( pCondition->GetName(), V_strlen( pCondition->GetName() ) ); + + m_ContextPrerequisites[i].pszContextName = AllocateString( pSubKey->GetName() ); + m_ContextPrerequisites[i].pszCondition = AllocateString( pCondition->GetString() ); + } + } + + //---------------------------------------------------------------------------- + // Virtual Words + //---------------------------------------------------------------------------- + KeyValues *pVirtualWords = pManifest->FindKey( "VirtualWords" ); + FOR_EACH_SUBKEY( pVirtualWords, pSubKey ) + { + // + // Determine if this has nested virtual words or not + // + KeyValues *pMatches = pSubKey->FindKey( "matches" ); + if (pMatches) + { + // + // It has nested virtual words + // + int i = m_VirtualWordLists.Insert( RemoveVirtualPrefix( pSubKey->GetName() ) ); + m_VirtualWordLists[i].pszCriterion = AllocateString( pSubKey->GetString( "criterion" ) ); + + bool bFirst = true; + FOR_EACH_SUBKEY( pMatches, pWordList ) + { + if (bFirst) + { + // First one is always default + if (LoadVirtualWordDefinitionFromKV( pWordList, m_VirtualWordLists[i].m_DefaultWordDefinition, pSubKey->GetName() )) + bFirst = false; + } + else + { + int i2 = m_VirtualWordLists[i].m_WordDefinitions.Insert( pWordList->GetName() ); + if (!LoadVirtualWordDefinitionFromKV( pWordList, m_VirtualWordLists[i].m_WordDefinitions[i2], pSubKey->GetName() )) + m_VirtualWordLists[i].m_WordDefinitions.RemoveAt( i2 ); + } + } + } + else + { + // + // It is a virtual word + // + int i = m_VirtualWords.Insert( RemoveVirtualPrefix( pSubKey->GetName() ) ); + if (!LoadVirtualWordDefinitionFromKV( pSubKey, m_VirtualWords[i], pSubKey->GetName() )) + m_VirtualWords.RemoveAt( i ); + } + } + + //---------------------------------------------------------------------------- + // Sentences + //---------------------------------------------------------------------------- + KeyValues *pSentences = pManifest->FindKey( "Sentences" ); + FOR_EACH_SUBKEY( pSentences, pSetKey ) + { + FOR_EACH_SUBKEY( pSetKey, pSubKey ) + { + if (pSubKey->GetName() && *pSubKey->GetString()) + { + int i = m_Sentences.Insert( pSubKey->GetName() ); + + char szPathName[MAX_PATH]; + V_strncpy( szPathName, pSetKey->GetName(), sizeof( szPathName ) ); + V_FixSlashes( szPathName ); + m_Sentences[i].pszPrefix = AllocateString( szPathName ); + + //m_Sentences[i].pszPrefix = AllocateString( pSetKey->GetName() ); + + ParseChoreoSentence( NULL, pSubKey->GetString(), m_Sentences[i] ); + m_Sentences[i].pszName = m_Sentences.GetElementName( i ); + } + } + } + } + pManifest->deleteThis(); + } + + bool LoadVirtualWordDefinitionFromKV( KeyValues *pWordList, VirtualWordDefinition_t &wordDefinition, const char *pszWordName ) + { + // + // Get the condition for this virtual word + // + KeyValues *pCondition = pWordList->FindKey( "condition" ); + if (pCondition) + { + int nStrLen = V_strlen( pCondition->GetString() ); + const char *pColon = V_strnchr( pCondition->GetString(), ':', nStrLen ); + if (pColon) + { + wordDefinition.pszCondition = AllocateString( pColon + 1 ); + wordDefinition.nConditionType = DeduceCondition( pCondition->GetString(), pColon - pCondition->GetString() ); + } + else + { + wordDefinition.nConditionType = DeduceCondition( pCondition->GetString(), nStrLen ); + } + } + else + { + wordDefinition.nConditionType = 0; + wordDefinition.pszCondition = ""; + } + + // + // Get the words themselves + // + KeyValues *pWords = pWordList->FindKey( "words" ); + if (pWords) + { + FOR_EACH_SUBKEY( pWords, pWord ) + { + int i = wordDefinition.m_Words.AddToTail(); + V_strncpy( wordDefinition.m_Words[i].szWord, pWord->GetString(), sizeof( wordDefinition.m_Words[i].szWord ) ); + } + } + else + { + pWords = pWordList->FindKey( "words_from" ); + if (pWords) + { + const char *pszWordsTarget = RemoveVirtualPrefix( pWords->GetString() ); + + // + // Get from another virtual word + // + FOR_EACH_DICT_FAST( m_VirtualWords, i ) + { + if (FStrEq( m_VirtualWords.GetElementName( i ), pszWordsTarget )) + { + wordDefinition.m_Words.AddVectorToTail( m_VirtualWords[i].m_Words ); + } + } + } + } + + if (wordDefinition.m_Words.Count() <= 0) + { + Warning( "WARNING: No words found on virtual word %s\n", pszWordName ); + return false; + } + + return true; + } + + //---------------------------------------------------------------------------- + + void PurgeSentences() + { + m_ContextPrerequisites.RemoveAll(); + m_VirtualWordLists.RemoveAll(); + m_VirtualWords.RemoveAll(); + m_Sentences.RemoveAll(); + + for (unsigned int i = 0; i < m_Strings.Count(); i++) + { + delete m_Strings[i]; + } + m_Strings.Purge(); + } + + //---------------------------------------------------------------------------- + + void PrintEverything() + { + Msg( "CONTEXT PREREQUISITES\n\n" ); + + FOR_EACH_VEC( m_ContextPrerequisites, i ) + { + Msg( "\t\"%s\"\n\t\tCondition: %i (\"%s\")\n\n", m_ContextPrerequisites[i].pszContextName, m_ContextPrerequisites[i].nConditionType, m_ContextPrerequisites[i].pszCondition ); + } + + Msg( "VIRTUAL WORDS\n\n" ); + + FOR_EACH_DICT_FAST( m_VirtualWords, i ) + { + char szWords[256] = { 0 }; + FOR_EACH_VEC( m_VirtualWords[i].m_Words, nWord ) + { + V_strncat( szWords, "\t\t\t\"", sizeof( szWords ) ); + V_strncat( szWords, m_VirtualWords[i].m_Words[nWord].szWord, sizeof( szWords ) ); + V_strncat( szWords, "\"\n", sizeof( szWords ) ); + } + + Msg( "\t\"%s\"\n\t\tCondition: %i (\"%s\")\n\t\tWords:\n%s\n\n", m_VirtualWords.GetElementName( i ), m_VirtualWords[i].nConditionType, m_VirtualWords[i].pszCondition, szWords ); + } + + Msg( "SENTENCES\n\n" ); + + FOR_EACH_DICT_FAST( m_Sentences, i ) + { + char szWords[128] = { 0 }; + FOR_EACH_VEC( m_Sentences[i].m_Words, nWord ) + { + if (m_Sentences[i].m_Words[nWord].nPitch != 100) + { + V_snprintf( szWords, sizeof( szWords ), "%s(p%i) ", szWords, m_Sentences[i].m_Words[nWord].nPitch ); + } + + if (m_Sentences[i].m_Words[nWord].nVol != 100) + { + V_snprintf( szWords, sizeof( szWords ), "%s(v%i) ", szWords, m_Sentences[i].m_Words[nWord].nVol ); + } + + V_strncat( szWords, "\"", sizeof( szWords ) ); + V_strncat( szWords, m_Sentences[i].m_Words[nWord].pszWord, sizeof( szWords ) ); + V_strncat( szWords, "\" ", sizeof( szWords ) ); + } + + Msg( "\t\"%s\"\n\t\tPrefix: \"%s\"\n\t\tCaption: \"%s\"\n\t\tWords: %s\n\n", m_Sentences.GetElementName( i ), m_Sentences[i].pszPrefix, m_Sentences[i].pszCaption ? m_Sentences[i].pszCaption : "N/A", szWords); + } + } + + void PrintStrings() + { + CUtlVector strings( 0, m_Strings.Count() ); + for ( UtlHashHandle_t i = m_Strings.FirstInorder(); i != m_Strings.InvalidIndex(); i = m_Strings.NextInorder(i) ) + { + strings.AddToTail( m_Strings[i] ); + } + + struct _Local { + static int __cdecl F(const char * const *a, const char * const *b) { return strcmp(*a, *b); } + }; + strings.Sort( _Local::F ); + + for ( int i = 0; i < strings.Count(); ++i ) + { + Msg( " %d (0x%p) : %s\n", i, strings[i], strings[i] ); + } + Msg( "\n" ); + Msg( "Size: %d items\n", strings.Count() ); + } + + //---------------------------------------------------------------------------- + + const ChoreoSentence_t *LookupChoreoSentence( CBaseEntity *pSpeaker, const char *pszSentenceName ) + { + FOR_EACH_DICT_FAST( m_Sentences, i ) + { + if (FStrEq( m_Sentences.GetElementName( i ), pszSentenceName )) + { + return &m_Sentences[i]; + } + } + + return NULL; + } + + //---------------------------------------------------------------------------- + + void PrecacheVirtualWord( const char *pszWord, const char *pszPrefix, const char *pszSuffix ) + { + pszWord = RemoveVirtualPrefix( pszWord ); + + FOR_EACH_DICT_FAST( m_VirtualWords, i ) + { + if (FStrEq( m_VirtualWords.GetElementName( i ), pszWord )) + { + // Precache all words + FOR_EACH_VEC( m_VirtualWords[i].m_Words, i2 ) + { + CBaseEntity::PrecacheScriptSound( UTIL_VarArgs( "%s%s%s", pszPrefix, m_VirtualWords[i].m_Words[i2].szWord, pszSuffix ) ); + } + return; + } + } + + FOR_EACH_DICT_FAST( m_VirtualWordLists, i ) + { + if (FStrEq( m_VirtualWordLists.GetElementName( i ), pszWord )) + { + // Precache all words in default definition + FOR_EACH_VEC( m_VirtualWordLists[i].m_DefaultWordDefinition.m_Words, i2 ) + { + CBaseEntity::PrecacheScriptSound( UTIL_VarArgs( "%s%s%s", pszPrefix, m_VirtualWordLists[i].m_DefaultWordDefinition.m_Words[i2].szWord, pszSuffix)); + } + + // Precache all words in nested definitions + FOR_EACH_DICT( m_VirtualWordLists[i].m_WordDefinitions, i2 ) + { + FOR_EACH_VEC( m_VirtualWordLists[i].m_WordDefinitions[i2].m_Words, i3 ) + { + CBaseEntity::PrecacheScriptSound( UTIL_VarArgs( "%s%s%s", pszPrefix, m_VirtualWordLists[i].m_WordDefinitions[i2].m_Words[i3].szWord, pszSuffix ) ); + } + } + return; + } + } + } + + inline const VirtualWord_t &ResolveVirtualWordDefinition( CBaseEntity *pSpeaker, const VirtualWordDefinition_t &wordDefinition ) + { + // Resolve this condition + int nIndex = ResolveVirtualCondition( pSpeaker, wordDefinition.nConditionType, wordDefinition.pszCondition, wordDefinition.m_Words.Count() ); + Assert( nIndex >= 0 && nIndex < wordDefinition.m_Words.Count() ); + + return wordDefinition.m_Words[nIndex]; + } + + const char *ResolveVirtualWord( CBaseEntity *pSpeaker, const char *pszWord ) + { + pszWord = RemoveVirtualPrefix( pszWord ); + + FOR_EACH_DICT_FAST( m_VirtualWords, i ) + { + if (FStrEq( m_VirtualWords.GetElementName( i ), pszWord )) + { + return ResolveVirtualWordDefinition( pSpeaker, m_VirtualWords[i] ).szWord; + } + } + + FOR_EACH_DICT_FAST( m_VirtualWordLists, i ) + { + if (FStrEq( m_VirtualWordLists.GetElementName( i ), pszWord )) + { + AI_CriteriaSet set; + if (pSpeaker) + pSpeaker->ModifyOrAppendCriteria( set ); + + int nCriterion = set.FindCriterionIndex( m_VirtualWordLists[i].pszCriterion ); + if (set.IsValidIndex( nCriterion )) + { + const char *pszValue = set.GetValue( nCriterion ); + + // Find the set of virtual words that matches the criterion + FOR_EACH_DICT( m_VirtualWordLists[i].m_WordDefinitions, i2 ) + { + if (Matcher_Match( m_VirtualWordLists[i].m_WordDefinitions.GetElementName(i2), pszValue )) + { + return ResolveVirtualWordDefinition( pSpeaker, m_VirtualWordLists[i].m_WordDefinitions[i2] ).szWord; + } + } + + // Return the default + return ResolveVirtualWordDefinition( pSpeaker, m_VirtualWordLists[i].m_DefaultWordDefinition ).szWord; + } + } + } + + CGWarning( 0, CON_GROUP_CHOREO, "Choreo sentence can't find virtual word \"%s\"\n", pszWord ); + return ""; + } + + int ResolveVirtualCondition( CBaseEntity *pSpeaker, int nConditionType, const char *pszCondition, int nWordCount ) + { + switch (nConditionType) + { + default: + case ConditionType_None: + // Randomize between each word + return RandomInt( 0, nWordCount - 1 ); + break; + + case ConditionType_Context: + // Return context as integer + if (pSpeaker) + { + if (pSpeaker->FindContextByName( pszCondition ) == -1) + { + // Check if this is a prerequisite, and if it is, then apply it + FOR_EACH_VEC( m_ContextPrerequisites, i ) + { + if (FStrEq( m_ContextPrerequisites[i].pszContextName, pszCondition )) + { + pSpeaker->AddContext( pszCondition, UTIL_VarArgs("%i", ResolveVirtualCondition(pSpeaker, m_ContextPrerequisites[i].nConditionType, m_ContextPrerequisites[i].pszCondition, nWordCount))); + } + } + } + + int nContext = clamp( atoi( pSpeaker->GetContextValue( pszCondition ) ), 0, nWordCount - 1 ); + return nContext; + } + break; + + case ConditionType_VScript: + { + // Return VScript code result + g_pScriptVM->SetValue( "_choreo_val", "" ); + + HSCRIPT hCreateChainScript = g_pScriptVM->CompileScript( UTIL_VarArgs( "_choreo_val = (%s)", pszCondition ) ); + g_pScriptVM->Run( hCreateChainScript, pSpeaker ? pSpeaker->GetOrCreatePrivateScriptScope() : NULL ); + + ScriptVariant_t scriptVar; + g_pScriptVM->GetValue( "_choreo_val", &scriptVar ); + + g_pScriptVM->ClearValue( "_choreo_val" ); + + if (scriptVar.m_int < 0 || scriptVar.m_int >= nWordCount) + { + Warning("Choreo sentence script var %i from '%s' out of range\n", scriptVar.m_int, pszCondition ); + scriptVar.m_int = 0; + } + + return scriptVar.m_int; + } + break; + + case ConditionType_DistTo: + case ConditionType_DirTo: + case ConditionType_GridX: + case ConditionType_GridY: + { + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, pszCondition, pSpeaker ); + if (pSpeaker && pTarget) + { + if (nConditionType == ConditionType_DistTo) + { + // Convert to meters + return (pSpeaker->GetAbsOrigin() - pTarget->GetAbsOrigin()).Length() / 52.49344; + } + else if (nConditionType == ConditionType_DirTo) + { + Vector vecDir = (pSpeaker->GetAbsOrigin() - pTarget->GetAbsOrigin()); + return vecDir.y; + } + else if (nConditionType == ConditionType_GridX) + { + return (int)(pTarget->GetAbsOrigin().x / 10.0f) % 20; + } + else if (nConditionType == ConditionType_GridY) + { + return (int)(pTarget->GetAbsOrigin().y / 10.0f) % 20; + } + } + } + break; + } + + return 0; + } + + //---------------------------------------------------------------------------- + + enum + { + ConditionType_None, // No condition (random) + ConditionType_Context, // Word should match value from specified context + ConditionType_VScript, // Word should match value returned from specified VScript code + + // HL2 sentence parameters + ConditionType_DistTo, // Word should match distance to specified target + ConditionType_DirTo, // Word should match direction to specified target + ConditionType_GridX, // Word should match world X axis grid of specified target + ConditionType_GridY, // Word should match world Y axis grid of specified target + }; + + int DeduceCondition( const char *pszCondition, int nNumChars ) + { + if (V_strncmp( pszCondition, "context", nNumChars ) == 0) + return ConditionType_Context; + else if (V_strncmp( pszCondition, "vscript", nNumChars ) == 0) + return ConditionType_VScript; + else if (V_strncmp( pszCondition, "dist_to", nNumChars ) == 0) + return ConditionType_DistTo; + else if (V_strncmp( pszCondition, "dir_to", nNumChars ) == 0) + return ConditionType_DirTo; + else if (V_strncmp( pszCondition, "gridx", nNumChars ) == 0) + return ConditionType_GridX; + else if (V_strncmp( pszCondition, "gridy", nNumChars ) == 0) + return ConditionType_GridY; + + return ConditionType_None; + } + + struct VirtualWord_t + { + char szWord[MAX_CHOREO_SENTENCE_VIRTUAL_WORD_LEN]; + }; + + struct VirtualWordDefinition_t + { + VirtualWordDefinition_t() {} + VirtualWordDefinition_t( const VirtualWordDefinition_t &src ) + { + pszCondition = src.pszCondition; + nConditionType = src.nConditionType; + m_Words.RemoveAll(); + m_Words.AddVectorToTail( src.m_Words ); + } + + const char *pszCondition; + int nConditionType; + + CUtlVector< VirtualWord_t > m_Words; + }; + + struct VirtualWordDefinitionList_t + { + VirtualWordDefinitionList_t() {} + VirtualWordDefinitionList_t( const VirtualWordDefinitionList_t &src ) + { + pszCriterion = src.pszCriterion; + m_DefaultWordDefinition = src.m_DefaultWordDefinition; + + m_WordDefinitions.RemoveAll(); + m_WordDefinitions.EnsureCapacity( src.m_WordDefinitions.Count() ); + FOR_EACH_DICT_FAST( src.m_WordDefinitions, i ) + m_WordDefinitions.Insert( src.m_WordDefinitions.GetElementName( i ), src.m_WordDefinitions[i] ); + } + + const char *pszCriterion; + + VirtualWordDefinition_t m_DefaultWordDefinition; + CUtlDict< VirtualWordDefinition_t, int > m_WordDefinitions; + }; + + struct SentenceContextPrerequisite_t + { + SentenceContextPrerequisite_t() {} + SentenceContextPrerequisite_t( const SentenceContextPrerequisite_t &src ) + { + pszContextName = src.pszContextName; + pszCondition = src.pszCondition; + nConditionType = src.nConditionType; + } + + const char *pszContextName; + const char *pszCondition; + int nConditionType; + }; + + //---------------------------------------------------------------------------- + + const char *FindString( const char *string ) + { + unsigned short i = m_Strings.Find( string ); + return i == m_Strings.InvalidIndex() ? NULL : m_Strings[i]; + } + + const char *AllocateString( const char *string ) + { + int i = m_Strings.Find( string ); + if ( i != m_Strings.InvalidIndex() ) + { + return m_Strings[i]; + } + + int len = Q_strlen( string ); + char *out = new char[ len + 1 ]; + Q_memcpy( out, string, len ); + out[ len ] = 0; + + return m_Strings[ m_Strings.Insert( out ) ]; + } + + //---------------------------------------------------------------------------- + + const CUtlDict< ChoreoSentence_t, int > &GetSentences() { return m_Sentences; } + +private: + + // Context prerequisites to be applied if not already applied + CUtlVector< SentenceContextPrerequisite_t > m_ContextPrerequisites; + + // Embedded lists of virtual words based on a criterion + CUtlDict< VirtualWordDefinitionList_t, int > m_VirtualWordLists; + + // Lists of virtual words (does not include nested ones) + CUtlDict< VirtualWordDefinition_t, int > m_VirtualWords; + + // Sentences themselves + CUtlDict< ChoreoSentence_t, int > m_Sentences; + + // Dedicated strings, copied from game string pool + CUtlRBTree m_Strings; + +}; + +static CChoreoSentenceSystem g_ChoreoSentenceSystem; + +//----------------------------------------------------------------------------- + +CON_COMMAND( choreosentence_reload, "Reloads choreo sentences" ) +{ + //int nStringCount = g_ChoreoSentenceSystem.GetStringCount(); + g_ChoreoSentenceSystem.PurgeSentences(); + + //g_ChoreoSentenceSystem.ReserveStrings( nStringCount ); + g_ChoreoSentenceSystem.LoadChoreoSentenceManifest( "scripts/choreosentences_manifest.txt" ); +} + +CON_COMMAND( choreosentence_dump, "Prints all choreo sentence stuff" ) +{ + g_ChoreoSentenceSystem.PrintEverything(); +} + +CON_COMMAND( choreosentence_dump_strings, "Prints all strings allocated by choreo sentences" ) +{ + g_ChoreoSentenceSystem.PrintStrings(); +} + +//------------------------------------------------------------------------------ +// Purpose : +//------------------------------------------------------------------------------ +static int AutoCompleteChoreoSentences(const char *cmdname, CUtlVector< CUtlString > &commands, CUtlRBTree< CUtlString > &symbols, char *substring, int checklen = 0) +{ + FOR_EACH_DICT_FAST( g_ChoreoSentenceSystem.GetSentences(), i ) + { + CUtlString sym = g_ChoreoSentenceSystem.GetSentences().GetElementName( i ); + + if (Q_strnicmp( sym, substring, checklen ) != 0) + continue; + + int idx = symbols.Find( sym ); + if (idx == symbols.InvalidIndex()) + { + symbols.Insert( sym ); + } + + // Too many + if (symbols.Count() >= COMMAND_COMPLETION_MAXITEMS) + break; + } + + // Now fill in the results + for (int i = symbols.FirstInorder(); i != symbols.InvalidIndex(); i = symbols.NextInorder(i)) + { + const char *name = symbols[i].String(); + + char buf[512]; + Q_snprintf( buf, sizeof( buf ), "%s %s", cmdname, name ); + Q_strlower( buf ); + + CUtlString command; + command = buf; + commands.AddToTail(command); + } + + return symbols.Count(); +} + +class CChoreoSentenceAutoCompletionFunctor : public ICommandCallback, public ICommandCompletionCallback +{ +public: + virtual void CommandCallback( const CCommand &command ) + { + if (command.ArgC() != 2) + { + Msg( "Format: choreosentence_play \n" ); + return; + } + + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if (pPlayer) + { + const ChoreoSentence_t *pSentence = LookupChoreoSentence( pPlayer, command.Arg( 1 ) ); + if (pSentence) + PrecacheChoreoSentence( *pSentence ); + + pPlayer->PlayChoreoSentenceScene( command.Arg( 1 ) ); + } + } + + virtual int CommandCompletionCallback( const char *partial, CUtlVector< CUtlString > &commands ) + { + if ( !g_pGameRules ) + { + return 0; + } + + const char *cmdname = "choreosentence_play"; + + char *substring = (char *)partial; + if ( Q_strstr( partial, cmdname ) ) + { + substring = (char *)partial + strlen( cmdname ) + 1; + } + + int checklen = Q_strlen( substring ); + + extern bool UtlStringLessFunc( const CUtlString & lhs, const CUtlString & rhs ); + CUtlRBTree< CUtlString > symbols( 0, 0, UtlStringLessFunc ); + + return AutoCompleteChoreoSentences(cmdname, commands, symbols, substring, checklen); + } +}; + +static CChoreoSentenceAutoCompletionFunctor g_ChoreoSentenceAutoComplete; +static ConCommand choreosentence_play("choreosentence_play", &g_ChoreoSentenceAutoComplete, "Plays the specified choreo sentence on the player", FCVAR_CHEAT, &g_ChoreoSentenceAutoComplete ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const char *ChoreoSentence_t::GetWordString( CBaseEntity *pSpeaker, int i ) const +{ + const char *pszExtension = ""; + if (V_strrchr( pszPrefix, CORRECT_PATH_SEPARATOR )) + { + // Use waves if our prefix is a path + pszExtension = ".wav"; + } + + // TODO: Something more elaborate than UTIL_VarArgs? + if (m_Words[i].bVirtual) + { + const char *pszVirtualWord = g_ChoreoSentenceSystem.ResolveVirtualWord( pSpeaker, m_Words[i].pszWord ); + return UTIL_VarArgs( "%s%s%s", pszPrefix, pszVirtualWord, pszExtension ); + } + + return UTIL_VarArgs( "%s%s%s", pszPrefix, m_Words[i].pszWord, pszExtension ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void LoadChoreoSentenceFile( const char *pszFile ) +{ + g_ChoreoSentenceSystem.LoadChoreoSentenceFile( pszFile ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const ChoreoSentence_t *LookupChoreoSentence( CBaseEntity *pSpeaker, const char *pszSentenceName ) +{ + return g_ChoreoSentenceSystem.LookupChoreoSentence( pSpeaker, pszSentenceName ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void PrecacheChoreoSentence( const ChoreoSentence_t &sentence ) +{ + FOR_EACH_VEC( sentence.m_Words, i ) + { + if (sentence.m_Words[i].bVirtual) + { + // Precache all virtual words + const char *pszExtension = ""; + if (V_strrchr( sentence.pszPrefix, CORRECT_PATH_SEPARATOR )) + pszExtension = ".wav"; + + g_ChoreoSentenceSystem.PrecacheVirtualWord( sentence.m_Words[i].pszWord, sentence.pszPrefix, pszExtension ); + } + else + { + CBaseEntity::PrecacheScriptSound( sentence.GetWordString( NULL, i ) ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool ParseChoreoSentence( CBaseEntity *pSpeaker, const char *pszRawSentence, ChoreoSentence_t &sentence ) +{ + if (pszRawSentence == NULL || *pszRawSentence == NULL) + return false; + + char szSentence[256]; + + // First, try getting the prefix + const char *pColon = V_strnchr( pszRawSentence, ':', V_strlen( pszRawSentence ) ); + if (pColon) + { + // Sentence is everything after colon + Q_strncpy( szSentence, pColon + 1, sizeof( szSentence ) ); + + // Copy everything before colon for prefix + char szPathName[MAX_PATH]; + V_strncpy( szPathName, pszRawSentence, pColon - pszRawSentence + 1 ); + V_FixSlashes( szPathName ); + sentence.pszPrefix = g_ChoreoSentenceSystem.AllocateString( szPathName ); + } + else + { + // It's all one sentence + Q_strncpy( szSentence, pszRawSentence, sizeof( szSentence ) ); + } + + // Now get any parameters + const char *pSemiColon = V_strnchr( szSentence, ';', sizeof( szSentence ) ); + if (pSemiColon) + { + // Caption is whatever's after the semicolon + const char *pszCaption = pSemiColon+1; + if (pszCaption[0] == ' ') + pszCaption++; + + sentence.pszCaption = g_ChoreoSentenceSystem.AllocateString( pszCaption ); + + // Replace semicolon with null terminator + szSentence[pSemiColon - szSentence] = '\0'; + } + + // Next, split up the sentence itself + bool success = ParseChoreoSentenceContents( pSpeaker, szSentence, sentence ); + + return success; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool ParseChoreoSentenceContents( CBaseEntity *pSpeaker, char *pszSentence, ChoreoSentence_t &sentence ) +{ + int nCurVol = 100, nCurPitch = 100; + + char *pszToken = strtok( pszSentence, " " ); + for (; pszToken != NULL; pszToken = strtok( NULL, " " )) + { + if (!pszToken || !*pszToken) + continue; + + // Check if this is a command number + if (pszToken[0] == '(') + { + pszToken++; + + // Remove closing parentheses + //int end = V_strlen( pszToken )-1; + //if (pszToken[end] == ')') + // pszToken[end] = '\0'; + + int nNum = atoi( pszToken + 1 ); + if (nNum > 0) + { + switch (pszToken[0]) + { + // TODO: Recognize e, t, etc.? + case 'v': nCurVol = nNum; break; + case 'p': nCurPitch = nNum; break; + } + continue; + } + else + { + Msg( "Zero command number in %s\n", pszSentence ); + } + } + + int nWord = sentence.m_Words.AddToTail(); + + sentence.m_Words[nWord].nVol = nCurVol; + sentence.m_Words[nWord].nPitch = nCurPitch; + + // Check if this is virtual + if (IsVirtualWord( pszToken )) + sentence.m_Words[nWord].bVirtual = true; + + // Check for periods or commas + int end = V_strlen( pszToken )-1; + if (pszToken[end] == ',') + { + int nWord2 = sentence.m_Words.AddToTail(); + sentence.m_Words[nWord2].pszWord = g_ChoreoSentenceSystem.AllocateString( "_comma" ); + sentence.m_Words[nWord2].nVol = nCurVol; + sentence.m_Words[nWord2].nPitch = nCurPitch; + pszToken[end] = '\0'; + } + else if (pszToken[end] == '.') + { + int nWord2 = sentence.m_Words.AddToTail(); + sentence.m_Words[nWord2].pszWord = g_ChoreoSentenceSystem.AllocateString( "_period" ); + sentence.m_Words[nWord2].nVol = nCurVol; + sentence.m_Words[nWord2].nPitch = nCurPitch; + pszToken[end] = '\0'; + } + + sentence.m_Words[nWord].pszWord = g_ChoreoSentenceSystem.AllocateString( pszToken ); + } + + return sentence.m_Words.Count() > 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float GetChoreoSentenceDuration( CBaseFlex *pSpeaker, const ChoreoSentence_t &sentence ) +{ + const char *actormodel = (pSpeaker ? STRING( pSpeaker->GetModelName() ) : NULL); + float flLength = 0.0f; + + FOR_EACH_VEC( sentence.m_Words, i ) + { + //float duration = CBaseEntity::GetSoundDuration( sentence.GetWordString(pSpeaker, i), actormodel ); + + float duration; + const char *pszWord = sentence.GetWordString( pSpeaker, i ); + + // For now, call the engine functions manually instead of using CBaseEntity::GetSoundDuration so that we could get around the WaveTrace warning + if ( V_stristr( pszWord, ".wav" ) || V_stristr( pszWord, ".mp3" ) ) + { + duration = enginesound->GetSoundDuration( PSkipSoundChars( pszWord ) ); + } + else + { + extern ISoundEmitterSystemBase *soundemitterbase; + duration = enginesound->GetSoundDuration( PSkipSoundChars( soundemitterbase->GetWavFileForSound( pszWord, actormodel ) ) ); + } + + flLength += duration; + } + + return flLength; +} diff --git a/sp/src/game/server/mapbase/choreosentence.h b/sp/src/game/server/mapbase/choreosentence.h new file mode 100644 index 0000000000..55371ed1e2 --- /dev/null +++ b/sp/src/game/server/mapbase/choreosentence.h @@ -0,0 +1,67 @@ +//========= Mapbase - https://github.com/mapbase-source/source-sdk-2013 ============// +// +// Purpose: VCD-based sentences. +// +//=============================================================================// + +#ifndef CHOREOSENTENCE_H +#define CHOREOSENTENCE_H + +#include "cbase.h" + + +#define MAX_CHOREO_SENTENCE_PREFIX_LEN 64 +#define MAX_CHOREO_SENTENCE_WORD_LEN 32 +#define MAX_CHOREO_SENTENCE_CAPTION_LEN 64 + +#define MAX_CHOREO_SENTENCE_VIRTUAL_WORD_LEN 32 + +struct ChoreoSentenceWord_t +{ + ChoreoSentenceWord_t() {} + ChoreoSentenceWord_t( const ChoreoSentenceWord_t &src ) + { + pszWord = src.pszWord; + nPitch = src.nPitch; nVol = src.nVol; + bVirtual = src.bVirtual; + } + + const char *pszWord; + int nPitch, nVol = 100; + bool bVirtual = false; +}; + +struct ChoreoSentence_t +{ + ChoreoSentence_t() {} + ChoreoSentence_t( const ChoreoSentence_t &src ) + { + pszName = src.pszName; + pszPrefix = src.pszPrefix; + pszCaption = src.pszCaption; + m_Words.RemoveAll(); + m_Words.AddVectorToTail( src.m_Words ); + } + + const char *GetWordString( CBaseEntity *pSpeaker, int i ) const; + + CUtlVector< ChoreoSentenceWord_t > m_Words; + const char *pszName; + const char *pszPrefix; + const char *pszCaption; +}; + +//---------------------------------------------------------------------------- + +extern void LoadChoreoSentenceFile( const char *pszFile ); + +extern const ChoreoSentence_t *LookupChoreoSentence( CBaseEntity *pSpeaker, const char *pszSentenceName ); + +extern void PrecacheChoreoSentence( const ChoreoSentence_t &sentence ); + +bool ParseChoreoSentence( CBaseEntity *pSpeaker, const char *pszRawSentence, ChoreoSentence_t &sentence ); +bool ParseChoreoSentenceContents( CBaseEntity *pSpeaker, char *pszSentence, ChoreoSentence_t &sentence ); + +extern float GetChoreoSentenceDuration( CBaseFlex *pSpeaker, const ChoreoSentence_t &sentence ); + +#endif // CHOREOSENTENCE_H diff --git a/sp/src/game/server/sceneentity.cpp b/sp/src/game/server/sceneentity.cpp index ab70e1974f..fd3bf6dd32 100644 --- a/sp/src/game/server/sceneentity.cpp +++ b/sp/src/game/server/sceneentity.cpp @@ -41,6 +41,10 @@ #include "npc_alyx_episodic.h" #endif // HL2_EPISODIC +#ifdef MAPBASE +#include "mapbase/choreosentence.h" +#endif + // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" @@ -482,6 +486,9 @@ class CSceneEntity : public CPointEntity, public IChoreoEventCallback bool InvolvesActor( CBaseEntity *pActor ); // NOTE: returns false if scene hasn't loaded yet void GenerateSoundScene( CBaseFlex *pActor, const char *soundname ); +#ifdef MAPBASE + void GenerateChoreoSentenceScene( CBaseFlex *pActor, const char *pszSentenceName ); +#endif virtual float GetPostSpeakDelay() { return 1.0; } @@ -594,6 +601,9 @@ class CSceneEntity : public CPointEntity, public IChoreoEventCallback void PrecacheScene( CChoreoScene *scene ); CChoreoScene *GenerateSceneForSound( CBaseFlex *pFlexActor, const char *soundname ); +#ifdef MAPBASE + CChoreoScene *GenerateSceneForSentenceName( CBaseFlex *pFlexActor, const char *pszSentenceName ); +#endif bool CheckActors(); @@ -643,6 +653,9 @@ class CSceneEntity : public CPointEntity, public IChoreoEventCallback bool m_bGenerated; string_t m_iszSoundName; CHandle< CBaseFlex > m_hActor; +#ifdef MAPBASE + bool m_bChoreoSentence; +#endif EHANDLE m_hActivator; @@ -736,6 +749,9 @@ BEGIN_DATADESC( CSceneEntity ) DEFINE_FIELD( m_bGenerated, FIELD_BOOLEAN ), DEFINE_FIELD( m_iszSoundName, FIELD_STRING ), DEFINE_FIELD( m_hActor, FIELD_EHANDLE ), +#ifdef MAPBASE + DEFINE_FIELD( m_bChoreoSentence, FIELD_BOOLEAN ), +#endif DEFINE_FIELD( m_hActivator, FIELD_EHANDLE ), // DEFINE_FIELD( m_bSceneMissing, FIELD_BOOLEAN ), @@ -953,6 +969,146 @@ CChoreoScene *CSceneEntity::GenerateSceneForSound( CBaseFlex *pFlexActor, const return scene; } +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +// Input : *actor - +// *pszSentenceName - +// Output : CChoreoScene +//----------------------------------------------------------------------------- +CChoreoScene *CSceneEntity::GenerateSceneForSentenceName( CBaseFlex *pFlexActor, const char *pszSentenceName ) +{ + const ChoreoSentence_t *pSentence = LookupChoreoSentence( pFlexActor, pszSentenceName ); + if ( !pSentence ) + { + Warning( "CSceneEntity::GenerateSceneForSentenceName: Couldn't find sentence '%s'\n", pszSentenceName ); + return NULL; + } + + // TODO: Raw sentence support? + // ChoreoSentence_t sentence; + // if ( !ParseChoreoSentence( pFlexActor, pszSentence, sentence ) ) + // { + // Warning( "CSceneEntity::GenerateSceneForSentence: Couldn't parse sentence from '%s'\n", pszSentence ); + // return NULL; + // } + + CChoreoScene *scene = new CChoreoScene( this ); + if ( !scene ) + { + Warning( "CSceneEntity::GenerateSceneForSentenceName: Failed to allocated new scene!!!\n" ); + } + else + { + scene->SetPrintFunc( LocalScene_Printf ); + + CChoreoActor *actor = scene->AllocActor(); + CChoreoChannel *channel = scene->AllocChannel(); + + Assert( actor ); + Assert( channel ); + + if ( !actor || !channel ) + { + Warning( "CSceneEntity::GenerateSceneForSentenceName: Alloc of actor or channel failed!!!\n" ); + delete scene; + return NULL; + } + + // Set us up the actorz + actor->SetName( "!self" ); // Could be pFlexActor->GetName()? + actor->SetActive( true ); + + // Set us up the channelz + channel->SetName( STRING( m_iszSceneFile ) ); + channel->SetActor( actor ); + + // Add to actor + actor->AddChannel( channel ); + + // Set us up the eventz + const char *actormodel = (pFlexActor ? STRING( pFlexActor->GetModelName() ) : NULL); + float flCurTime = 0.0f; + FOR_EACH_VEC( pSentence->m_Words, i ) + { + const char *pszWord = pSentence->GetWordString( pFlexActor, i ); + + float duration = CBaseEntity::GetSoundDuration( pszWord, actormodel ); + if (duration <= 0.0f) + { + Warning( "CSceneEntity::GenerateSceneForSentenceName: Couldn't determine duration of %s\n", pszWord ); + } + + CChoreoEvent *event = scene->AllocEvent(); + Assert( event ); + + if ( !event ) + { + Warning( "CSceneEntity::GenerateSceneForSentenceName: Alloc of event failed!!!\n" ); + delete scene; + return NULL; + } + + if (pSentence->pszCaption) + { + // First word gets the caption, others fall back to it + if (i == 0) + { + event->SetCloseCaptionType( CChoreoEvent::CC_MASTER ); + event->SetCloseCaptionToken( pSentence->pszCaption ); + } + else + { + event->SetCloseCaptionType( CChoreoEvent::CC_SLAVE ); + } + } + //else if (pSentence->pszName) + //{ + // // TODO: Caption from name? + //} + + if (pSentence->m_Words[i].nVol != 100) + { + event->SetYaw( pSentence->m_Words[i].nVol ); + } + + if (pSentence->m_Words[i].nPitch != 100) + { + duration *= (100.0f / ((float)pSentence->m_Words[i].nPitch)); + event->SetPitch( pSentence->m_Words[i].nPitch ); + } + + // HACKHACK: Need to be spaced away from repeated sound to avoid changing the previous sound's pitch instead + if (i+1 < pSentence->m_Words.Count() && pSentence->m_Words[i+1].pszWord == pSentence->m_Words[i].pszWord + && pSentence->m_Words[i + 1].nPitch != 100) + duration += 0.1f; + + event->SetType( CChoreoEvent::SPEAK ); + event->SetName( pszWord ); + event->SetParameters( pszWord ); + event->SetStartTime( flCurTime ); + event->SetUsingRelativeTag( false ); + event->SetEndTime( flCurTime + duration ); + event->SnapTimes(); + + //Msg( "%i %s: %f -> %f (%f)\n", i, pszWord, flCurTime, flCurTime + duration, duration ); + + // Add to channel + channel->AddEvent( event ); + + // Point back to our owners + event->SetChannel( channel ); + event->SetActor( actor ); + + flCurTime += duration; + } + + } + + return scene; +} +#endif + //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- @@ -960,6 +1116,13 @@ void CSceneEntity::Activate() { if ( m_bGenerated && !m_pScene ) { +#ifdef MAPBASE + if (m_bChoreoSentence) + { + m_pScene = GenerateSceneForSentenceName( m_hActor, STRING( m_iszSoundName ) ); + } + else +#endif m_pScene = GenerateSceneForSound( m_hActor, STRING( m_iszSoundName ) ); } @@ -1138,6 +1301,22 @@ void CSceneEntity::GenerateSoundScene( CBaseFlex *pActor, const char *soundname m_hActor = pActor; } +#ifdef MAPBASE +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pActor - +// *pszSentenceName - +//----------------------------------------------------------------------------- +void CSceneEntity::GenerateChoreoSentenceScene( CBaseFlex *pActor, const char *pszSentenceName ) +{ + m_bGenerated = true; + m_iszSoundName = MAKE_STRING( pszSentenceName ); + m_hActor = pActor; + + m_bChoreoSentence = true; +} +#endif + //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. @@ -1930,6 +2109,26 @@ void CSceneEntity::DispatchStartSpeak( CChoreoScene *scene, CBaseFlex *actor, CC // Warning( "Speak %s\n", soundname ); +#ifdef MAPBASE + if ( m_fPitch != 1.0f || event->GetPitch() != 0 ) + { + if ( es.m_nPitch && es.m_nPitch != 100 ) + es.m_nPitch = static_cast( es.m_nPitch ) * m_fPitch; + else + { + float flPitch = (event->GetPitch() != 0 ? event->GetPitch() : 100.0f); + es.m_nPitch = flPitch * m_fPitch; + } + + es.m_nFlags |= SND_CHANGE_PITCH; + } + + if ( event->GetYaw() != 0 ) + { + es.m_flVolume = (((float)event->GetYaw()) / 100.0f); + es.m_nFlags |= SND_CHANGE_VOL; + } +#else if ( m_fPitch != 1.0f ) { if ( es.m_nPitch ) @@ -1939,6 +2138,7 @@ void CSceneEntity::DispatchStartSpeak( CChoreoScene *scene, CBaseFlex *actor, CC es.m_nFlags |= SND_CHANGE_PITCH; } +#endif EmitSound( filter2, actor->entindex(), es ); actor->AddSceneEvent( scene, event ); @@ -5650,6 +5850,62 @@ void CInstancedSceneEntity::OnLoaded() #endif } +#ifdef MAPBASE +float InstancedChoreoSentenceScene( CBaseFlex *pActor, const char *pszSentence, EHANDLE *phSceneEnt, + float flPostDelay, bool bIsBackground, AI_Response *response, + bool bMultiplayer, IRecipientFilter *filter /* = NULL */ ) +{ + if ( !pActor ) + { + Warning( "InstancedChoreoSentenceScene: Expecting non-NULL pActor for sound %s\n", pszSentence ); + return 0; + } + + CInstancedSceneEntity *pScene = (CInstancedSceneEntity *)CBaseEntity::CreateNoSpawn( "instanced_scripted_scene", vec3_origin, vec3_angle ); + + Q_strncpy( pScene->m_szInstanceFilename, UTIL_VarArgs( "AutoGenerated(%s)", pszSentence ), sizeof( pScene->m_szInstanceFilename ) ); + pScene->m_iszSceneFile = MAKE_STRING( pScene->m_szInstanceFilename ); + + pScene->m_hOwner = pActor; + pScene->m_bHadOwner = pActor != NULL; + + pScene->GenerateChoreoSentenceScene( pActor, pszSentence ); + + pScene->m_bMultiplayer = bMultiplayer; + pScene->SetPostSpeakDelay( flPostDelay ); + DispatchSpawn( pScene ); + pScene->Activate(); + pScene->m_bIsBackground = bIsBackground; + + pScene->SetBackground( bIsBackground ); + pScene->SetRecipientFilter( filter ); + + if ( response ) + { + float flPreDelay = response->GetPreDelay(); + if ( flPreDelay ) + { + pScene->SetPreDelay( flPreDelay ); + } + } + + pScene->StartPlayback(); + + if ( response ) + { + // If the response wants us to abort on NPC state switch, remember that + pScene->SetBreakOnNonIdle( response->ShouldBreakOnNonIdle() ); + } + + if ( phSceneEnt ) + { + *phSceneEnt = pScene; + } + + return pScene->EstimateLength(); +} +#endif + bool g_bClientFlex = true; LINK_ENTITY_TO_CLASS( scene_manager, CSceneManager ); diff --git a/sp/src/game/server/sceneentity.h b/sp/src/game/server/sceneentity.h index ea286fe95f..8ab19a77a9 100644 --- a/sp/src/game/server/sceneentity.h +++ b/sp/src/game/server/sceneentity.h @@ -28,6 +28,7 @@ int GetRecentNPCSpeech( recentNPCSpeech_t speech[ SPEECH_LIST_MAX_SOUNDS ] ); float InstancedScriptedScene( CBaseFlex *pActor, const char *pszScene, EHANDLE *phSceneEnt = NULL, float flPostDelay = 0.0f, bool bIsBackground = false, AI_Response *response = NULL, bool bMultiplayer = false, IRecipientFilter *filter = NULL ); #ifdef MAPBASE float InstancedAutoGeneratedSoundScene( CBaseFlex *pActor, char const *soundname, EHANDLE *phSceneEnt = NULL, float flPostDelay = 0.0f, bool bIsBackground = false, AI_Response *response = NULL, bool bMultiplayer = false, IRecipientFilter *filter = NULL ); +float InstancedChoreoSentenceScene( CBaseFlex *pActor, char const *pszSentence, EHANDLE *phSceneEnt = NULL, float flPostDelay = 0.0f, bool bIsBackground = false, AI_Response *response = NULL, bool bMultiplayer = false, IRecipientFilter *filter = NULL ); #else float InstancedAutoGeneratedSoundScene( CBaseFlex *pActor, char const *soundname, EHANDLE *phSceneEnt = NULL ); #endif diff --git a/sp/src/game/server/server_mapbase.vpc b/sp/src/game/server/server_mapbase.vpc index 7d74847e12..892b91a6d1 100644 --- a/sp/src/game/server/server_mapbase.vpc +++ b/sp/src/game/server/server_mapbase.vpc @@ -63,6 +63,8 @@ $Project $File "mapbase\ai_grenade.h" $File "mapbase\ai_monitor.cpp" $File "mapbase\ai_weaponmodifier.cpp" + $File "mapbase\choreosentence.cpp" + $File "mapbase\choreosentence.h" $File "mapbase\custom_weapon_factory.cpp" $File "mapbase\custom_weapon_factory.h" $File "mapbase\closecaption_entity.cpp" diff --git a/sp/src/game/shared/mapbase/mapbase_shared.cpp b/sp/src/game/shared/mapbase/mapbase_shared.cpp index 8c364b62ea..ccbd7b37b9 100644 --- a/sp/src/game/shared/mapbase/mapbase_shared.cpp +++ b/sp/src/game/shared/mapbase/mapbase_shared.cpp @@ -29,6 +29,7 @@ #include "AI_ResponseSystem.h" #include "mapbase/SystemConvarMod.h" #include "gameinterface.h" +#include "mapbase/choreosentence.h" #endif // memdbgon must be the last include file in a .cpp file!!! @@ -107,7 +108,7 @@ enum MANIFEST_HUDLAYOUT, #else MANIFEST_TALKER, - //MANIFEST_SENTENCES, + MANIFEST_CHOREOSENTENCES, MANIFEST_ACTBUSY, #endif #ifdef MAPBASE_VSCRIPT @@ -147,7 +148,7 @@ static const ManifestType_t gm_szManifestFileStrings[MANIFEST_NUM_TYPES] = { { "hudlayout", "mapbase_load_hudlayout", "Should we load map-specific HUD layout overrides? e.g. \"maps/_hudlayout.res\"" }, #else { "talker", "mapbase_load_talker", "Should we load map-specific talker files? e.g. \"maps/_talker.txt\"" }, - //{ "sentences", "mapbase_load_sentences", "Should we load map-specific sentences? e.g. \"maps/_sentences.txt\"" }, + { "choreosentences", "mapbase_load_choreosentences", "Should we load map-specific choreo sentences? e.g. \"maps/_choreosentences.txt\"" }, { "actbusy", "mapbase_load_actbusy", "Should we load map-specific actbusy files? e.g. \"maps/_actbusy.txt\"" }, #endif #ifdef MAPBASE_VSCRIPT @@ -465,7 +466,7 @@ class CMapbaseSystem : public CAutoGameSystem LoadResponseSystemFile(value); //PrecacheCustomResponseSystem( value ); } break; //case MANIFEST_SOUNDSCAPES: { g_SoundscapeSystem.AddSoundscapeFile(value); } break; - //case MANIFEST_SENTENCES: { engine->PrecacheSentenceFile(value); } break; + case MANIFEST_CHOREOSENTENCES: { LoadChoreoSentenceFile(value); } break; case MANIFEST_ACTBUSY: { ParseCustomActbusyFile(value); } break; #endif #ifdef MAPBASE_VSCRIPT @@ -610,6 +611,7 @@ class CMapbaseSystem : public CAutoGameSystem #else void LoadCustomTalkerFile( const char *szScript ) { LoadFromValue( szScript, MANIFEST_TALKER, false ); } void LoadCustomActbusyFile( const char *szScript ) { LoadFromValue( szScript, MANIFEST_ACTBUSY, false ); } + void LoadCustomChoreoSentenceFile( const char *szScript ) { LoadFromValue( szScript, MANIFEST_CHOREOSENTENCES, false ); } #endif const char *GetModName() { return g_iszGameName; } @@ -658,6 +660,7 @@ BEGIN_SCRIPTDESC_ROOT( CMapbaseSystem, SCRIPT_SINGLETON "All-purpose Mapbase sys #else DEFINE_SCRIPTFUNC( LoadCustomTalkerFile, "Loads a custom talker file." ) DEFINE_SCRIPTFUNC( LoadCustomActbusyFile, "Loads a custom actbusy file." ) + DEFINE_SCRIPTFUNC( LoadCustomChoreoSentenceFile, "Loads a custom choreo sentence file." ) #endif DEFINE_SCRIPTFUNC( GetModName, "Gets the name of the mod. This is the name which shows up on Steam, RPC, etc." ) diff --git a/sp/src/public/responserules/response_types.h b/sp/src/public/responserules/response_types.h index 2e80cc5aae..d30def807f 100644 --- a/sp/src/public/responserules/response_types.h +++ b/sp/src/public/responserules/response_types.h @@ -99,6 +99,7 @@ namespace ResponseRules #ifdef MAPBASE RESPONSE_VSCRIPT, // Run VScript code RESPONSE_VSCRIPT_FILE, // Run a VScript file (bypasses ugliness and character limits when just using IncludeScript() with RESPONSE_VSCRIPT) + RESPONSE_CHOREOSENTENCE, // Sentences put together as VCDs, see choreosentence.h for more information #endif NUM_RESPONSES, diff --git a/sp/src/responserules/runtime/response_system.cpp b/sp/src/responserules/runtime/response_system.cpp index 7ae9276c79..ad87962c23 100644 --- a/sp/src/responserules/runtime/response_system.cpp +++ b/sp/src/responserules/runtime/response_system.cpp @@ -1643,6 +1643,8 @@ inline ResponseType_t ComputeResponseType( const char *s ) return RESPONSE_VSCRIPT_FILE; else return RESPONSE_VSCRIPT; + case 'c': + return RESPONSE_CHOREOSENTENCE; #endif } diff --git a/sp/src/responserules/runtime/rr_response.cpp b/sp/src/responserules/runtime/rr_response.cpp index b652e8b377..eb21a55b5e 100644 --- a/sp/src/responserules/runtime/rr_response.cpp +++ b/sp/src/responserules/runtime/rr_response.cpp @@ -260,6 +260,8 @@ const char *CRR_Response::DescribeResponse( ResponseType_t type ) return "RESPONSE_VSCRIPT"; case ResponseRules::RESPONSE_VSCRIPT_FILE: return "RESPONSE_VSCRIPT_FILE"; + case ResponseRules::RESPONSE_CHOREOSENTENCE: + return "RESPONSE_CHOREOSENTENCE"; #endif }