//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: The system for handling director's commentary style production info in-game. // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "tier0/icommandline.h" #include "igamesystem.h" #include "filesystem.h" #include #include "in_buttons.h" #include "engine/IEngineSound.h" #include "soundenvelope.h" #include "utldict.h" #include "isaverestore.h" #include "eventqueue.h" #include "saverestore_utlvector.h" #include "GameStats.h" #include "ai_basenpc.h" #include "Sprite.h" #include "point_template.h" #include "mapentities.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" static bool g_bTracingVsCommentaryNodes = false; static const char *s_pCommentaryUpdateViewThink = "CommentaryUpdateViewThink"; #define COMMENTARY_SPAWNED_SEMAPHORE "commentary_semaphore" extern ConVar commentary; ConVar commentary_available("commentary_available", "0", FCVAR_NONE, "Automatically set by the game when a commentary file is available for the current map." ); extern ConVar rr_remarkables_enabled; enum teleport_stages_t { TELEPORT_NONE, TELEPORT_FADEOUT, TELEPORT_TELEPORT, TELEPORT_FADEIN, }; // Convar restoration save/restore #define MAX_MODIFIED_CONVAR_STRING 128 struct modifiedconvars_t { DECLARE_SIMPLE_DATADESC(); char pszConvar[MAX_MODIFIED_CONVAR_STRING]; char pszCurrentValue[MAX_MODIFIED_CONVAR_STRING]; char pszOrgValue[MAX_MODIFIED_CONVAR_STRING]; }; bool g_bInCommentaryMode = false; bool IsInCommentaryMode( void ) { return g_bInCommentaryMode; } PRECACHE_REGISTER_BEGIN( GLOBAL, PrecachePointCommentaryNode ) PRECACHE( MODEL, "models/extras/info_speech.mdl" ) PRECACHE_REGISTER_END() //----------------------------------------------------------------------------- // Purpose: An entity that marks a spot for a piece of commentary //----------------------------------------------------------------------------- class CPointCommentaryNode : public CBaseAnimating { DECLARE_CLASS( CPointCommentaryNode, CBaseAnimating ); public: DECLARE_DATADESC(); DECLARE_SERVERCLASS(); CPointCommentaryNode(); void Spawn( void ); void Precache( void ); void Activate( void ); void SpinThink( void ); void StartCommentary( void ); void FinishCommentary( bool bBlendOut = true ); void CleanupPostCommentary( void ); void UpdateViewThink( void ); void UpdateViewPostThink( void ); bool TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ); bool HasViewTarget( void ) { return (m_hViewTarget != NULL || m_hViewPosition.Get() != NULL); } bool PreventsMovement( void ); bool CannotBeStopped( void ) { return (m_bUnstoppable || m_bPreventChangesWhileMoving); } int UpdateTransmitState( void ); void SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ); void SetDisabled( bool bDisabled ); void SetNodeNumber( int iCount ) { m_iNodeNumber = iCount; } // Called to tell the node when it's moved under/not-under the player's crosshair void SetUnderCrosshair( bool bUnderCrosshair ); // Called when the player attempts to activate the node void PlayerActivated( void ); void StopPlaying( void ); void AbortPlaying( void ); void TeleportTo( CBasePlayer *pPlayer ); bool CanTeleportTo( void ); // Inputs void InputStartCommentary( inputdata_t &inputdata ); void InputStartUnstoppableCommentary( inputdata_t &inputdata ); void InputEnable( inputdata_t &inputdata ); void InputDisable( inputdata_t &inputdata ); private: string_t m_iszPreCommands; string_t m_iszPostCommands; CNetworkVar( string_t, m_iszCommentaryFile ); CNetworkVar( string_t, m_iszCommentaryFileNoHDR ); string_t m_iszViewTarget; EHANDLE m_hViewTarget; EHANDLE m_hViewTargetAngles; // Entity used to blend view angles to look at the target string_t m_iszViewPosition; CNetworkVar( EHANDLE, m_hViewPosition ); EHANDLE m_hViewPositionMover; // Entity used to blend the view to the viewposition entity bool m_bPreventMovement; bool m_bUnderCrosshair; bool m_bUnstoppable; float m_flFinishedTime; Vector m_vecFinishOrigin; QAngle m_vecOriginalAngles; QAngle m_vecFinishAngles; bool m_bPreventChangesWhileMoving; bool m_bDisabled; Vector m_vecTeleportOrigin; COutputEvent m_pOnCommentaryStarted; COutputEvent m_pOnCommentaryStopped; CNetworkVar( bool, m_bActive ); CNetworkVar( float, m_flStartTime ); CNetworkVar( string_t, m_iszSpeakers ); CNetworkVar( int, m_iNodeNumber ); CNetworkVar( int, m_iNodeNumberMax ); }; BEGIN_DATADESC( CPointCommentaryNode ) DEFINE_KEYFIELD( m_iszPreCommands, FIELD_STRING, "precommands" ), DEFINE_KEYFIELD( m_iszPostCommands, FIELD_STRING, "postcommands" ), DEFINE_KEYFIELD( m_iszCommentaryFile, FIELD_STRING, "commentaryfile" ), DEFINE_KEYFIELD( m_iszCommentaryFileNoHDR, FIELD_STRING, "commentaryfile_nohdr" ), DEFINE_KEYFIELD( m_iszViewTarget, FIELD_STRING, "viewtarget" ), DEFINE_FIELD( m_hViewTarget, FIELD_EHANDLE ), DEFINE_FIELD( m_hViewTargetAngles, FIELD_EHANDLE ), DEFINE_KEYFIELD( m_iszViewPosition, FIELD_STRING, "viewposition" ), DEFINE_FIELD( m_hViewPosition, FIELD_EHANDLE ), DEFINE_FIELD( m_hViewPositionMover, FIELD_EHANDLE ), DEFINE_KEYFIELD( m_bPreventMovement, FIELD_BOOLEAN, "prevent_movement" ), DEFINE_FIELD( m_bUnderCrosshair, FIELD_BOOLEAN ), DEFINE_FIELD( m_bUnstoppable, FIELD_BOOLEAN ), DEFINE_FIELD( m_flFinishedTime, FIELD_TIME ), DEFINE_FIELD( m_vecFinishOrigin, FIELD_VECTOR ), DEFINE_FIELD( m_vecOriginalAngles, FIELD_VECTOR ), DEFINE_FIELD( m_vecFinishAngles, FIELD_VECTOR ), DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ), DEFINE_FIELD( m_flStartTime, FIELD_TIME ), DEFINE_KEYFIELD( m_iszSpeakers, FIELD_STRING, "speakers" ), DEFINE_FIELD( m_iNodeNumber, FIELD_INTEGER ), DEFINE_FIELD( m_iNodeNumberMax, FIELD_INTEGER ), DEFINE_FIELD( m_bPreventChangesWhileMoving, FIELD_BOOLEAN ), DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "start_disabled" ), DEFINE_KEYFIELD( m_vecTeleportOrigin, FIELD_VECTOR, "teleport_origin" ), // Outputs DEFINE_OUTPUT( m_pOnCommentaryStarted, "OnCommentaryStarted" ), DEFINE_OUTPUT( m_pOnCommentaryStopped, "OnCommentaryStopped" ), // Inputs DEFINE_INPUTFUNC( FIELD_VOID, "StartCommentary", InputStartCommentary ), DEFINE_INPUTFUNC( FIELD_VOID, "StartUnstoppableCommentary", InputStartUnstoppableCommentary ), DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), // Functions DEFINE_THINKFUNC( SpinThink ), DEFINE_THINKFUNC( UpdateViewThink ), DEFINE_THINKFUNC( UpdateViewPostThink ), END_DATADESC() IMPLEMENT_SERVERCLASS_ST( CPointCommentaryNode, DT_PointCommentaryNode ) SendPropBool( SENDINFO(m_bActive) ), SendPropStringT( SENDINFO(m_iszCommentaryFile) ), SendPropStringT( SENDINFO(m_iszCommentaryFileNoHDR) ), SendPropTime( SENDINFO(m_flStartTime) ), SendPropStringT( SENDINFO(m_iszSpeakers) ), SendPropInt( SENDINFO(m_iNodeNumber), 8, SPROP_UNSIGNED ), SendPropInt( SENDINFO(m_iNodeNumberMax), 8, SPROP_UNSIGNED ), SendPropEHandle( SENDINFO(m_hViewPosition) ), END_SEND_TABLE() LINK_ENTITY_TO_CLASS( point_commentary_node, CPointCommentaryNode ); //----------------------------------------------------------------------------- // Laser Dot //----------------------------------------------------------------------------- class CCommentaryViewPosition : public CSprite { DECLARE_CLASS( CCommentaryViewPosition, CSprite ); public: virtual void Spawn( void ) { Precache(); SetModelName( MAKE_STRING("sprites/redglow1.vmt") ); BaseClass::Spawn(); SetMoveType( MOVETYPE_NONE ); AddSolidFlags( FSOLID_NOT_SOLID ); AddEffects( EF_NOSHADOW ); UTIL_SetSize( this, vec3_origin, vec3_origin ); } virtual void Precache( void ) { PrecacheModel( "sprites/redglow1.vmt" ); } }; LINK_ENTITY_TO_CLASS( point_commentary_viewpoint, CCommentaryViewPosition ); //----------------------------------------------------------------------------- // Purpose: In multiplayer, always return player 1 //----------------------------------------------------------------------------- CBasePlayer *GetCommentaryPlayer( void ) { CBasePlayer *pPlayer; if ( gpGlobals->maxClients <= 1 ) { pPlayer = UTIL_GetLocalPlayer(); } else { // only respond to the first player pPlayer = UTIL_PlayerByIndex(1); } return pPlayer; } //=========================================================================================================== // COMMENTARY GAME SYSTEM //=========================================================================================================== void CV_GlobalChange_Commentary( IConVar *var, const char *pOldString, float flOldValue ); //----------------------------------------------------------------------------- // Purpose: Game system to kickstart the director's commentary //----------------------------------------------------------------------------- class CCommentarySystem : public CAutoGameSystemPerFrame { public: DECLARE_DATADESC(); CCommentarySystem() : CAutoGameSystemPerFrame( "CCommentarySystem" ) { m_iCommentaryNodeCount = 0; m_pkvSavedModifications = NULL; } virtual void LevelInitPreEntity() { m_hCurrentNode = NULL; m_bCommentaryConvarsChanging = false; m_iClearPressedButtons = 0; // If the map started via the map_commentary cmd, start in commentary g_bInCommentaryMode = (engine->IsInCommentaryMode() != 0); CalculateCommentaryState(); } void CalculateCommentaryState( void ) { // Set the available cvar if we can find commentary data for this level char szFullName[512]; Q_snprintf(szFullName,sizeof(szFullName), "maps/%s_commentary.txt", STRING( gpGlobals->mapname) ); if ( filesystem->FileExists( szFullName ) ) { commentary_available.SetValue( true ); // If the user wanted commentary, kick it on if ( commentary.GetBool() ) { g_bInCommentaryMode = true; } } else { g_bInCommentaryMode = false; commentary_available.SetValue( false ); } } virtual void LevelShutdownPreEntity() { ShutDownCommentary(); } void ParseEntKVBlock( CBaseEntity *pNode, KeyValues *pkvNode ) { KeyValues *pkvNodeData = pkvNode->GetFirstSubKey(); while ( pkvNodeData ) { // Handle the connections block if ( !Q_strcmp(pkvNodeData->GetName(), "connections") ) { ParseEntKVBlock( pNode, pkvNodeData ); } else { #define COMMENTARY_STRING_LENGTH_MAX 1024 const char *pszValue = pkvNodeData->GetString(); Assert( Q_strlen(pszValue) < COMMENTARY_STRING_LENGTH_MAX ); if ( Q_strnchr(pszValue, '^', COMMENTARY_STRING_LENGTH_MAX) ) { // We want to support quotes in our strings so that we can specify multiple parameters in // an output inside our commentary files. We convert ^s to "s here. char szTmp[COMMENTARY_STRING_LENGTH_MAX]; Q_strncpy( szTmp, pszValue, COMMENTARY_STRING_LENGTH_MAX ); int len = Q_strlen( szTmp ); for ( int i = 0; i < len; i++ ) { if ( szTmp[i] == '^' ) { szTmp[i] = '"'; } } pNode->KeyValue( pkvNodeData->GetName(), szTmp ); } else { pNode->KeyValue( pkvNodeData->GetName(), pszValue ); } } pkvNodeData = pkvNodeData->GetNextKey(); } } virtual void LevelInitPostEntity( void ) { // Previously, we would bail out immediately. However if info_remarkables are enabled, // there is the possiblity that they are defined in the commentary file, because it was a // more convenient markup tool for the writers than Hammer. Therefore we must load it. if ( !IsInCommentaryMode() && !rr_remarkables_enabled.GetBool() ) return; // Don't spawn commentary entities when loading a savegame if ( gpGlobals->eLoadType == MapLoad_LoadGame || gpGlobals->eLoadType == MapLoad_Background ) return; m_bCommentaryEnabledMidGame = false; InitCommentary(); if ( IsInCommentaryMode() ) { IGameEvent *event = gameeventmanager->CreateEvent( "playing_commentary" ); gameeventmanager->FireEventClientSide( event ); } } CPointCommentaryNode *GetNodeUnderCrosshair() { CBasePlayer *pPlayer = GetCommentaryPlayer(); if ( !pPlayer ) return NULL; // See if the player's looking at a commentary node trace_t tr; Vector vecSrc = pPlayer->EyePosition(); Vector vecForward = pPlayer->GetAutoaimVector( AUTOAIM_SCALE_DIRECT_ONLY ); g_bTracingVsCommentaryNodes = true; UTIL_TraceLine( vecSrc, vecSrc + vecForward * MAX_TRACE_LENGTH, MASK_SOLID, pPlayer, COLLISION_GROUP_NONE, &tr ); g_bTracingVsCommentaryNodes = false; if ( !tr.m_pEnt ) return NULL; return dynamic_cast(tr.m_pEnt); } void PrePlayerRunCommand( CBasePlayer *pPlayer, CUserCmd *pUserCmds ) { if ( !IsInCommentaryMode() ) return; if ( pPlayer->IsFakeClient() ) return; CPointCommentaryNode *pCurrentNode = GetNodeUnderCrosshair(); // Changed nodes? if ( m_hCurrentNode != pCurrentNode ) { // Stop animating the old one if ( m_hCurrentNode.Get() ) { m_hCurrentNode->SetUnderCrosshair( false ); } // Start animating the new one if ( pCurrentNode ) { pCurrentNode->SetUnderCrosshair( true ); } m_hCurrentNode = pCurrentNode; } // Check for commentary node activations if ( pPlayer ) { // Has the player pressed down an attack button? int buttonsChanged = m_afPlayersLastButtons ^ pUserCmds->buttons; int buttonsPressed = buttonsChanged & pUserCmds->buttons; m_afPlayersLastButtons = pUserCmds->buttons; if ( !(pUserCmds->buttons & COMMENTARY_BUTTONS) ) { m_iClearPressedButtons &= ~COMMENTARY_BUTTONS; } // Detect press events to start/stop commentary nodes if (buttonsPressed & COMMENTARY_BUTTONS) { #ifdef PORTAL2 if ( buttonsPressed & IN_REMOTE_VIEW && (V_stristr( MapName(), "coop" ) != NULL) ) #else if ( buttonsPressed & IN_ATTACK2 ) #endif { if ( !(GetActiveNode() && GetActiveNode()->CannotBeStopped()) ) { JumpToNextNode( pPlayer ); pUserCmds->buttons &= ~COMMENTARY_BUTTONS; m_iClearPressedButtons |= (buttonsPressed & COMMENTARY_BUTTONS); } } else { // Looking at a node? if ( m_hCurrentNode ) { // Ignore input while an unstoppable node is playing if ( !GetActiveNode() || !GetActiveNode()->CannotBeStopped() ) { // If we have an active node already, stop it if ( GetActiveNode() && GetActiveNode() != m_hCurrentNode ) { GetActiveNode()->StopPlaying(); } m_hCurrentNode->PlayerActivated(); } // Prevent weapon firing when toggling nodes pUserCmds->buttons &= ~COMMENTARY_BUTTONS; m_iClearPressedButtons |= (buttonsPressed & COMMENTARY_BUTTONS); } else if ( GetActiveNode() && GetActiveNode()->HasViewTarget() ) { if ( !GetActiveNode()->CannotBeStopped() ) { GetActiveNode()->StopPlaying(); } // Prevent weapon firing when toggling nodes pUserCmds->buttons &= ~COMMENTARY_BUTTONS; m_iClearPressedButtons |= (buttonsPressed & COMMENTARY_BUTTONS); } } } if ( GetActiveNode() && GetActiveNode()->PreventsMovement() ) { pUserCmds->buttons &= ~(IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT | IN_JUMP | IN_DUCK ); pUserCmds->upmove = 0; pUserCmds->sidemove = 0; pUserCmds->forwardmove = 0; } // When we swallow button down events, we have to keep clearing that button // until the player releases the button. Otherwise, the frame after we swallow // it, the code detects the button down and goes ahead as normal. pUserCmds->buttons &= ~m_iClearPressedButtons; } if ( m_iTeleportStage != TELEPORT_NONE ) { if ( m_flNextTeleportTime <= gpGlobals->curtime ) { if ( m_iTeleportStage == TELEPORT_FADEOUT ) { m_iTeleportStage = TELEPORT_TELEPORT; m_flNextTeleportTime = gpGlobals->curtime + 0.35; color32_s clr = { 0,0,0,255 }; UTIL_ScreenFade( pPlayer, clr, 0.3, 0, FFADE_OUT | FFADE_PURGE | FFADE_STAYOUT ); } else if ( m_iTeleportStage == TELEPORT_TELEPORT ) { if ( m_hLastCommentaryNode ) { m_hLastCommentaryNode->TeleportTo( pPlayer ); } m_iTeleportStage = TELEPORT_FADEIN; m_flNextTeleportTime = gpGlobals->curtime + 0.6; } else if ( m_iTeleportStage == TELEPORT_FADEIN ) { m_iTeleportStage = TELEPORT_NONE; m_flNextTeleportTime = gpGlobals->curtime + 0.25; color32_s clr = { 0,0,0,255 }; UTIL_ScreenFade( pPlayer, clr, 0.3, 0, FFADE_IN | FFADE_PURGE ); } } } } CPointCommentaryNode *GetActiveNode( void ) { return m_hActiveCommentaryNode; } void SetActiveNode( CPointCommentaryNode *pNode ) { m_hActiveCommentaryNode = pNode; if ( pNode ) { m_hLastCommentaryNode = pNode; } } int GetCommentaryNodeCount( void ) { return m_iCommentaryNodeCount; } bool CommentaryConvarsChanging( void ) { return m_bCommentaryConvarsChanging; } void SetCommentaryConvarsChanging( bool bChanging ) { m_bCommentaryConvarsChanging = bChanging; } void ConvarChanged( IConVar *pConVar, const char *pOldString, float flOldValue ) { ConVarRef var( pConVar ); // A convar has been changed by a commentary node. We need to store // the old state. If the engine shuts down, we need to restore any // convars that the commentary changed to their previous values. for ( int i = 0; i < m_ModifiedConvars.Count(); i++ ) { // If we find it, just update the current value if ( !Q_strncmp( var.GetName(), m_ModifiedConvars[i].pszConvar, MAX_MODIFIED_CONVAR_STRING ) ) { Q_strncpy( m_ModifiedConvars[i].pszCurrentValue, var.GetString(), MAX_MODIFIED_CONVAR_STRING ); //Msg(" Updating Convar %s: value %s (org %s)\n", m_ModifiedConvars[i].pszConvar, m_ModifiedConvars[i].pszCurrentValue, m_ModifiedConvars[i].pszOrgValue ); return; } } // We didn't find it in our list, so add it modifiedconvars_t newConvar; Q_strncpy( newConvar.pszConvar, var.GetName(), MAX_MODIFIED_CONVAR_STRING ); Q_strncpy( newConvar.pszCurrentValue, var.GetString(), MAX_MODIFIED_CONVAR_STRING ); Q_strncpy( newConvar.pszOrgValue, pOldString, MAX_MODIFIED_CONVAR_STRING ); m_ModifiedConvars.AddToTail( newConvar ); /* Msg(" Commentary changed '%s' to '%s' (was '%s')\n", var->GetName(), var->GetString(), pOldString ); Msg(" Convars stored: %d\n", m_ModifiedConvars.Count() ); for ( int i = 0; i < m_ModifiedConvars.Count(); i++ ) { Msg(" Convar %d: %s, value %s (org %s)\n", i, m_ModifiedConvars[i].pszConvar, m_ModifiedConvars[i].pszCurrentValue, m_ModifiedConvars[i].pszOrgValue ); } */ } void InitCommentary( void ) { MEM_ALLOC_CREDIT(); // Install the global cvar callback cvar->InstallGlobalChangeCallback( CV_GlobalChange_Commentary ); m_flNextTeleportTime = 0; m_iTeleportStage = TELEPORT_NONE; m_hLastCommentaryNode = NULL; // If we find the commentary semaphore, the commentary entities already exist. // This occurs when you transition back to a map that has saved commentary nodes in it. if ( gEntList.FindEntityByName( NULL, COMMENTARY_SPAWNED_SEMAPHORE ) ) return; // Spawn the commentary semaphore entity static CGameString infoTargetStr( "info_target" ); static CGameString commentarySpawnedSemaphoreName( COMMENTARY_SPAWNED_SEMAPHORE ); CBaseEntity *pSemaphore = CreateEntityByName( infoTargetStr ); pSemaphore->SetName( commentarySpawnedSemaphoreName ); bool oldLock = engine->LockNetworkStringTables( false ); if ( m_pkvSavedModifications != NULL ) { m_pkvSavedModifications->deleteThis(); m_pkvSavedModifications = NULL; } // Find the commentary file char szFullName[512]; Q_snprintf(szFullName,sizeof(szFullName), "maps/%s_commentary.txt", STRING( gpGlobals->mapname )); KeyValues *pkvFile = new KeyValues( "Commentary" ); if ( pkvFile->LoadFromFile( filesystem, szFullName, "MOD" ) ) { Msg( "Commentary: Loading commentary data from %s. \n", szFullName ); unsigned int iFileSize = filesystem->Size( szFullName, "MOD" ); Assert( iFileSize > 0 ); CPointTemplate *PointTemplates[MAX_EDICTS]; int iPointTemplateCount = 0; HierarchicalSpawnMapData_t SpawnData[MAX_EDICTS]; //for point templates //to avoid rewriting a bunch of code, I'm leaving the keyvalue parsing as is. But to get point_templates to work, I need the keyvalues as strings. So we go file->KeyValues->String. char *pKVToStringBuffer = (char *)stackalloc( sizeof(char *) * iFileSize ); char TempKeyBuffer[20 * 1024]; CUtlBuffer SpawnKeyValuesBackToString( TempKeyBuffer, 20 * 1024 ); int iKVStringBufferIndex = 0; // Load each commentary block, and spawn the entities KeyValues *pkvNode = pkvFile->GetFirstSubKey(); while ( pkvNode ) { // Get node name const char *pNodeName = pkvNode->GetName(); // Skip the trackinfo if ( StringHasPrefixCaseSensitive( pNodeName, "trackinfo" ) ) { pkvNode = pkvNode->GetNextKey(); continue; } KeyValues *pClassname = pkvNode->FindKey( "classname" ); if ( pClassname ) { // Use the classname instead pNodeName = pClassname->GetString(); } if ( rr_remarkables_enabled.GetBool() ) { bool bModifyBlock = !Q_strcmp( pkvNode->GetName(), "modify_entity" ); // in L4D, we hijack the commentary system to create // info_remarkable nodes. If we are here with commentary // disabled, load only those nodes. // We also hijack it for map updates (exploits/etc.) on 360 KeyValues *pUpdateFlag = pkvNode->FindKey( "mapupdate", false ); if ( !IsInCommentaryMode() && !StringHasPrefixCaseSensitive( pNodeName, "info_remarkable" ) && !pUpdateFlag && !bModifyBlock ) { // Move to next entity pkvNode = pkvNode->GetNextKey(); continue; } if ( bModifyBlock ) { if ( m_pkvSavedModifications == NULL ) { m_pkvSavedModifications = new KeyValues("Entities"); } m_pkvSavedModifications->AddSubKey( pkvNode->MakeCopy() ); } } // Spawn the commentary entity CBaseEntity *pNode = CreateEntityByName( pNodeName ); if ( pNode ) { ParseEntKVBlock( pNode, pkvNode ); EHANDLE hHandle; hHandle = pNode; int iAddIndex = m_hSpawnedEntities.AddToTail( hHandle ); SpawnKeyValuesBackToString.SeekPut( CUtlBuffer::SEEK_HEAD, 0 ); pkvNode->RecursiveSaveToFile( SpawnKeyValuesBackToString, 0 ); int iLength = SpawnKeyValuesBackToString.TellPut() - 1; //trim off the start and end that we don't care about while( TempKeyBuffer[iLength] != '}' ) { --iLength; } --iLength; char *pStart = TempKeyBuffer; while( *pStart != '{' ) { ++pStart; } pStart += 2; //RecursiveSaveToFile saves "name"\n{\n. We're skipping past that final \n iLength -= (pStart - TempKeyBuffer); //don't need to null terminate or space at all memcpy( &pKVToStringBuffer[iKVStringBufferIndex], pStart, iLength ); SpawnData[iAddIndex].m_pMapData = &pKVToStringBuffer[iKVStringBufferIndex]; SpawnData[iAddIndex].m_iMapDataLength = iLength; iKVStringBufferIndex += iLength; CPointCommentaryNode *pCommNode = dynamic_cast(pNode); if ( pCommNode ) { m_iCommentaryNodeCount++; pCommNode->SetNodeNumber( m_iCommentaryNodeCount ); } CPointTemplate *pPointTemplate = dynamic_cast(pNode); if ( pPointTemplate ) { PointTemplates[iPointTemplateCount] = pPointTemplate; ++iPointTemplateCount; } } else { Warning("Commentary: Failed to spawn commentary entity, type: '%s'\n", pNodeName ); } // Move to next entity pkvNode = pkvNode->GetNextKey(); } ApplyCommentaryModifications(); if( iPointTemplateCount != 0 ) { CBaseEntity **pSpawnedEntities = NULL; int iSpawnedEntityCount = m_hSpawnedEntities.Count(); if( iSpawnedEntityCount != 0 ) { pSpawnedEntities = (CBaseEntity **)stackalloc( sizeof(CBaseEntity *) * m_hSpawnedEntities.Count() ); for( int i = 0; i != iSpawnedEntityCount; ++i ) { pSpawnedEntities[i] = m_hSpawnedEntities[i].Get(); } } MapEntity_ParseAllEntites_SpawnTemplates( PointTemplates, iPointTemplateCount, pSpawnedEntities, SpawnData, iSpawnedEntityCount ); for( int i = iSpawnedEntityCount; --i >= 0; ) { //remove any entities that were nullified in the point template parsing process if( pSpawnedEntities[i] == NULL ) m_hSpawnedEntities.Remove(i); } } for( int i = 0; i != m_hSpawnedEntities.Count(); ++i ) { DispatchSpawn( m_hSpawnedEntities[i] ); } PrecachePointTemplates(); // Then activate all the entities for ( int i = 0; i < m_hSpawnedEntities.Count(); i++ ) { m_hSpawnedEntities[i]->Activate(); } } else { Msg( "Commentary: Could not find commentary data file '%s'. \n", szFullName ); } pkvFile->deleteThis(); engine->LockNetworkStringTables( oldLock ); } void ApplyCommentaryModifications() { KeyValues *pkvNode = m_pkvSavedModifications ? m_pkvSavedModifications->GetFirstSubKey() : NULL; while ( pkvNode ) { // See if this section has a modify sub group in it. KeyValues *pModifyBlock = pkvNode->FindKey( "modify" ); if ( pModifyBlock ) { // find entities with model name KeyValues *pFindByModelname = pkvNode->FindKey( "find_by_modelname" ); if ( pFindByModelname ) { CBaseEntity *pEnt = gEntList.FindEntityByModel( NULL, pFindByModelname->GetString() ); while ( pEnt ) { KeyValues *pKey = pModifyBlock->GetFirstSubKey(); while( pKey ) { pEnt->KeyValue( pKey->GetName(), pKey->GetString() ); pKey = pKey->GetNextKey(); } pEnt = gEntList.FindEntityByModel( pEnt, pFindByModelname->GetString() ); } } KeyValues *pFindByTargetname = pkvNode->FindKey( "find_by_targetname" ); if ( pFindByTargetname ) { CBaseEntity *pTarget = gEntList.FindEntityByTarget( NULL, pFindByTargetname->GetString() ); while ( pTarget ) { KeyValues *pKey = pModifyBlock->GetFirstSubKey(); while( pKey ) { pTarget->KeyValue( pKey->GetName(), pKey->GetString() ); pKey = pKey->GetNextKey(); } pTarget = gEntList.FindEntityByTarget( pTarget, pFindByTargetname->GetString() ); } } KeyValues *pFindByClassname = pkvNode->FindKey( "find_by_classname" ); if ( pFindByClassname ) { const char *className = pFindByClassname->GetString(); KeyValues *pCurrKey = pkvNode->GetFirstSubKey(); CBaseEntity* ent = NULL; // Walk through and select as many entities of this class as number of modify groups in this modify_entity block. while ( ( ent = gEntList.FindEntityByClassname( ent, className ) ) != NULL && pCurrKey ) { // We're on the next entity in the game that is of this class. // Find the next modify block in this group. while ( pCurrKey && V_stricmp( pCurrKey->GetName(), "modify" ) ) { pCurrKey = pCurrKey->GetNextKey(); } if ( NULL != pCurrKey ) { // If we have a valid modify block, copy the values from the block into this entity. KeyValues *pKey = pCurrKey->GetFirstSubKey(); while( pKey ) { ent->KeyValue( pKey->GetName(), pKey->GetString() ); pKey = pKey->GetNextKey(); } // Advance to the next key to see if it's a "modify" group in the next loop iteration. pCurrKey = pCurrKey->GetNextKey(); } } } } // Move to next entity pkvNode = pkvNode->GetNextKey(); } } void ShutDownCommentary( void ) { if ( GetActiveNode() ) { GetActiveNode()->AbortPlaying(); } // Destroy all the entities created by commentary for ( int i = m_hSpawnedEntities.Count()-1; i >= 0; i-- ) { if ( m_hSpawnedEntities[i] ) { UTIL_Remove( m_hSpawnedEntities[i] ); } } m_hSpawnedEntities.Purge(); m_iCommentaryNodeCount = 0; // Remove the commentary semaphore CBaseEntity *pSemaphore = gEntList.FindEntityByName( NULL, COMMENTARY_SPAWNED_SEMAPHORE ); if ( pSemaphore ) { UTIL_Remove( pSemaphore ); } // Remove our global convar callback cvar->RemoveGlobalChangeCallback( CV_GlobalChange_Commentary ); // Reset any convars that have been changed by the commentary for ( int i = 0; i < m_ModifiedConvars.Count(); i++ ) { ConVar *pConVar = (ConVar *)cvar->FindVar( m_ModifiedConvars[i].pszConvar ); if ( pConVar ) { pConVar->SetValue( m_ModifiedConvars[i].pszOrgValue ); } } m_ModifiedConvars.Purge(); if ( m_pkvSavedModifications ) { m_pkvSavedModifications->deleteThis(); } m_pkvSavedModifications = NULL; m_hCurrentNode = NULL; m_hActiveCommentaryNode = NULL; m_hLastCommentaryNode = NULL; m_flNextTeleportTime = 0; m_iTeleportStage = TELEPORT_NONE; } void SetCommentaryMode( bool bCommentaryMode ) { g_bInCommentaryMode = bCommentaryMode; CalculateCommentaryState(); // If we're turning on commentary, create all the entities. if ( IsInCommentaryMode() ) { m_bCommentaryEnabledMidGame = true; InitCommentary(); } else { ShutDownCommentary(); } } void OnRestore( void ) { cvar->RemoveGlobalChangeCallback( CV_GlobalChange_Commentary ); if ( !IsInCommentaryMode() ) return; // Set any convars that have already been changed by the commentary before the save for ( int i = 0; i < m_ModifiedConvars.Count(); i++ ) { ConVar *pConVar = (ConVar *)cvar->FindVar( m_ModifiedConvars[i].pszConvar ); if ( pConVar ) { //Msg(" Restoring Convar %s: value %s (org %s)\n", m_ModifiedConvars[i].pszConvar, m_ModifiedConvars[i].pszCurrentValue, m_ModifiedConvars[i].pszOrgValue ); pConVar->SetValue( m_ModifiedConvars[i].pszCurrentValue ); } } // Install the global cvar callback cvar->InstallGlobalChangeCallback( CV_GlobalChange_Commentary ); } bool CommentaryWasEnabledMidGame( void ) { return m_bCommentaryEnabledMidGame; } void JumpToNextNode( CBasePlayer *pPlayer ) { if ( m_flNextTeleportTime > gpGlobals->curtime || m_iTeleportStage != TELEPORT_NONE ) return; CBaseEntity *pEnt = m_hLastCommentaryNode; while ( ( pEnt = gEntList.FindEntityByClassname( pEnt, "point_commentary_node" ) ) != m_hLastCommentaryNode ) { CPointCommentaryNode *pNode = dynamic_cast( pEnt ); if ( pNode && pNode->CanTeleportTo() ) { m_iTeleportStage = TELEPORT_FADEOUT; m_hLastCommentaryNode = pNode; m_flNextTeleportTime = gpGlobals->curtime; // Stop any active nodes if ( m_hActiveCommentaryNode ) { m_hActiveCommentaryNode->StopPlaying(); } break; } } } private: int m_afPlayersLastButtons; int m_iCommentaryNodeCount; bool m_bCommentaryConvarsChanging; int m_iClearPressedButtons; bool m_bCommentaryEnabledMidGame; float m_flNextTeleportTime; int m_iTeleportStage; KeyValues *m_pkvSavedModifications; CUtlVector< modifiedconvars_t > m_ModifiedConvars; CUtlVector m_hSpawnedEntities; CHandle m_hCurrentNode; CHandle m_hActiveCommentaryNode; CHandle m_hLastCommentaryNode; friend bool Commentary_IsCommentaryEntity( CBaseEntity *pEntity ); }; CCommentarySystem g_CommentarySystem; void ApplyCommentaryModifications() { g_CommentarySystem.ApplyCommentaryModifications(); } bool Commentary_IsCommentaryEntity( CBaseEntity *pEntity ) { for( int i = 0; i != g_CommentarySystem.m_hSpawnedEntities.Count(); ++i ) { if( pEntity == g_CommentarySystem.m_hSpawnedEntities[i].Get() ) return true; } if( pEntity->GetEntityName() == MAKE_STRING( COMMENTARY_SPAWNED_SEMAPHORE ) ) { return true; } return false; } void Commentary_AbortCurrentNode( void ) { if ( g_CommentarySystem.GetActiveNode() ) { g_CommentarySystem.GetActiveNode()->AbortPlaying(); } } void CommentarySystem_PePlayerRunCommand( CBasePlayer *player, CUserCmd *ucmd ) { g_CommentarySystem.PrePlayerRunCommand( player, ucmd ); } BEGIN_DATADESC_NO_BASE( CCommentarySystem ) //int m_afPlayersLastButtons; DON'T SAVE //bool m_bCommentaryConvarsChanging; DON'T SAVE //int m_iClearPressedButtons; DON'T SAVE DEFINE_FIELD( m_bCommentaryEnabledMidGame, FIELD_BOOLEAN ), DEFINE_FIELD( m_flNextTeleportTime, FIELD_TIME ), DEFINE_FIELD( m_iTeleportStage, FIELD_INTEGER ), DEFINE_UTLVECTOR( m_ModifiedConvars, FIELD_EMBEDDED ), DEFINE_UTLVECTOR( m_hSpawnedEntities, FIELD_EHANDLE ), DEFINE_FIELD( m_hCurrentNode, FIELD_EHANDLE ), DEFINE_FIELD( m_hActiveCommentaryNode, FIELD_EHANDLE ), DEFINE_FIELD( m_hLastCommentaryNode, FIELD_EHANDLE ), DEFINE_FIELD( m_iCommentaryNodeCount, FIELD_INTEGER ), END_DATADESC() BEGIN_SIMPLE_DATADESC( modifiedconvars_t ) DEFINE_ARRAY( pszConvar, FIELD_CHARACTER, MAX_MODIFIED_CONVAR_STRING ), DEFINE_ARRAY( pszCurrentValue, FIELD_CHARACTER, MAX_MODIFIED_CONVAR_STRING ), DEFINE_ARRAY( pszOrgValue, FIELD_CHARACTER, MAX_MODIFIED_CONVAR_STRING ), END_DATADESC() //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CC_CommentaryChanged( IConVar *pConVar, const char *pOldString, float flOldValue ) { ConVarRef var( pConVar ); if ( var.GetBool() != g_bInCommentaryMode ) { g_CommentarySystem.SetCommentaryMode( var.GetBool() ); } } ConVar commentary("commentary", "0", 0, "Desired commentary mode state.", CC_CommentaryChanged ); //----------------------------------------------------------------------------- // Purpose: We need to revert back any convar changes that are made by the // commentary system during commentary. This code stores convar changes // made by the commentary system, and reverts them when finished. //----------------------------------------------------------------------------- void CV_GlobalChange_Commentary( IConVar *var, const char *pOldString, float flOldValue ) { if ( !g_CommentarySystem.CommentaryConvarsChanging() ) { // A convar has changed, but not due to commentary nodes. Ignore it. return; } g_CommentarySystem.ConvarChanged( var, pOldString, flOldValue ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CC_CommentaryNotChanging( void ) { g_CommentarySystem.SetCommentaryConvarsChanging( false ); } static ConCommand commentary_cvarsnotchanging("commentary_cvarsnotchanging", CC_CommentaryNotChanging, 0 ); bool IsListeningToCommentary( void ) { return ( g_CommentarySystem.GetActiveNode() != NULL ); } //=========================================================================================================== // COMMENTARY NODES //=========================================================================================================== CPointCommentaryNode::CPointCommentaryNode() : m_iNodeNumber( 0 ) { } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::Spawn( void ) { // No model specified? char *szModel = (char *)STRING( GetModelName() ); if (!szModel || !*szModel) { szModel = "models/extras/info_speech.mdl"; SetModelName( AllocPooledString(szModel) ); } Precache(); SetModel( szModel ); UTIL_SetSize( this, -Vector(16,16,16), Vector(16,16,16) ); SetSolid( SOLID_BBOX ); AddSolidFlags( FSOLID_CUSTOMRAYTEST | FSOLID_CUSTOMBOXTEST ); AddEffects( EF_NOSHADOW ); // Setup for animation ResetSequence( LookupSequence("idle") ); SetThink( &CPointCommentaryNode::SpinThink ); SetNextThink( gpGlobals->curtime + 0.1f ); m_iNodeNumberMax = 0; SetDisabled( m_bDisabled ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::Activate( void ) { m_iNodeNumberMax = g_CommentarySystem.GetCommentaryNodeCount(); if ( m_iszViewTarget != NULL_STRING ) { m_hViewTarget = gEntList.FindEntityByName( NULL, m_iszViewTarget ); if ( !m_hViewTarget ) { Warning("%s: %s could not find viewtarget %s.\n", GetClassname(), GetDebugName(), STRING(m_iszViewTarget) ); } } if ( m_iszViewPosition != NULL_STRING ) { m_hViewPosition = gEntList.FindEntityByName( NULL, m_iszViewPosition ); if ( !m_hViewPosition.Get() ) { Warning("%s: %s could not find viewposition %s.\n", GetClassname(), GetDebugName(), STRING(m_iszViewPosition) ); } } BaseClass::Activate(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::Precache() { PrecacheModel( STRING( GetModelName() ) ); if ( m_iszCommentaryFile.Get() != NULL_STRING ) { PrecacheScriptSound( STRING( m_iszCommentaryFile.Get() ) ); } else { Warning("%s: %s has no commentary file.\n", GetClassname(), GetDebugName() ); } if ( m_iszCommentaryFileNoHDR.Get() != NULL_STRING ) { PrecacheScriptSound( STRING( m_iszCommentaryFileNoHDR.Get() ) ); } BaseClass::Precache(); } //----------------------------------------------------------------------------- // Purpose: Called to tell the node when it's moved under/not-under the player's crosshair //----------------------------------------------------------------------------- void CPointCommentaryNode::SetUnderCrosshair( bool bUnderCrosshair ) { if ( bUnderCrosshair ) { // Start animating m_bUnderCrosshair = true; if ( !m_bActive ) { m_flAnimTime = gpGlobals->curtime; } } else { // Stop animating m_bUnderCrosshair = false; } } //------------------------------------------------------------------------------ // Purpose: Prevent collisions of everything except the trace from the commentary system //------------------------------------------------------------------------------ bool CPointCommentaryNode::TestCollision( const Ray_t &ray, unsigned int mask, trace_t& trace ) { if ( !g_bTracingVsCommentaryNodes ) return false; if ( m_bDisabled ) return false; return BaseClass::TestCollision( ray, mask, trace ); } //------------------------------------------------------------------------------ // Purpose: //------------------------------------------------------------------------------ void CPointCommentaryNode::SpinThink( void ) { // Rotate if we're active, or under the crosshair. Don't rotate if we're // under the crosshair, but we've already been listened to. if ( m_bActive || (m_bUnderCrosshair && m_nSkin == 0) ) { if ( m_bActive ) { m_flPlaybackRate = 3.0; } else { m_flPlaybackRate = 1.0; } StudioFrameAdvance(); DispatchAnimEvents(this); } SetNextThink( gpGlobals->curtime + 0.1f ); } //------------------------------------------------------------------------------ // Purpose: //------------------------------------------------------------------------------ void CPointCommentaryNode::PlayerActivated( void ) { #ifndef _GAMECONSOLE gamestats->Event_Commentary(); #endif if ( m_bActive ) { StopPlaying(); } else { StartCommentary(); g_CommentarySystem.SetActiveNode( this ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::StopPlaying( void ) { if ( m_bActive ) { FinishCommentary(); } } //----------------------------------------------------------------------------- // Purpose: Stop playing the node, but snap completely out of the node. // Used when players shut down commentary while we're in the middle // of playing a node, so we can't smoothly blend out (since the // commentary entities need to be removed). //----------------------------------------------------------------------------- void CPointCommentaryNode::AbortPlaying( void ) { if ( m_bActive ) { FinishCommentary( false ); } else if ( m_bPreventChangesWhileMoving ) { // We're a node that's not active, but is in the process of transitioning the view. Finish movement. CleanupPostCommentary(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CPointCommentaryNode::CanTeleportTo( void ) { //return ( m_vecTeleportOrigin != vec3_origin ); return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::TeleportTo( CBasePlayer *pPlayer ) { Vector vecTarget = m_vecTeleportOrigin; if ( m_vecTeleportOrigin == vec3_origin ) { vecTarget = GetAbsOrigin(); } trace_t trace; UTIL_TraceHull( vecTarget, vecTarget + Vector( 0, 0, -500 ), pPlayer->WorldAlignMins(), pPlayer->WorldAlignMaxs(), MASK_SOLID, pPlayer, COLLISION_GROUP_NONE, &trace ); pPlayer->Teleport( &trace.endpos, NULL, &vec3_origin ); Vector vecToNode = GetAbsOrigin() - pPlayer->EyePosition(); VectorNormalize( vecToNode ); QAngle vecAngle; VectorAngles( vecToNode, Vector(0,0,1), vecAngle ); pPlayer->SnapEyeAngles( vecAngle ); } //------------------------------------------------------------------------------ // Purpose: //------------------------------------------------------------------------------ void CPointCommentaryNode::StartCommentary( void ) { CBasePlayer *pPlayer = GetCommentaryPlayer(); if ( !pPlayer ) return; m_bActive = true; m_flAnimTime = gpGlobals->curtime; m_flPrevAnimTime = gpGlobals->curtime; // Switch to the greyed out skin m_nSkin = 1; m_pOnCommentaryStarted.FireOutput( this, this ); // Fire off our precommands if ( m_iszPreCommands != NULL_STRING ) { g_CommentarySystem.SetCommentaryConvarsChanging( true ); engine->ClientCommand( pPlayer->edict(), "%s", STRING(m_iszPreCommands) ); engine->ClientCommand( pPlayer->edict(), "commentary_cvarsnotchanging\n" ); } // Start the commentary m_flStartTime = gpGlobals->curtime; // If we have a view target, start blending towards it if ( m_hViewTarget || m_hViewPosition.Get() ) { m_vecOriginalAngles = pPlayer->EyeAngles(); SetContextThink( &CPointCommentaryNode::UpdateViewThink, gpGlobals->curtime, s_pCommentaryUpdateViewThink ); } //SetContextThink( &CPointCommentaryNode::FinishCommentary, gpGlobals->curtime + flDuration, s_pFinishCommentaryThink ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CC_CommentaryFinishNode( void ) { // We were told by the client DLL that our commentary has finished if ( g_CommentarySystem.GetActiveNode() ) { g_CommentarySystem.GetActiveNode()->StopPlaying(); } } static ConCommand commentary_finishnode("commentary_finishnode", CC_CommentaryFinishNode, 0 ); //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::UpdateViewThink( void ) { if ( !m_bActive ) return; CBasePlayer *pPlayer = GetCommentaryPlayer(); if ( !pPlayer ) return; // Swing the view towards the target if ( m_hViewTarget ) { if ( !m_hViewTargetAngles && !m_hViewPositionMover ) { // Make an invisible entity to attach view angles to m_hViewTargetAngles = CreateEntityByName( "point_commentary_viewpoint" ); m_hViewTargetAngles->SetAbsOrigin( pPlayer->EyePosition() ); m_hViewTargetAngles->SetAbsAngles( pPlayer->EyeAngles() ); pPlayer->SetViewEntity( m_hViewTargetAngles ); if ( pPlayer->GetActiveWeapon() ) { pPlayer->GetActiveWeapon()->Holster(); } } QAngle angGoal; QAngle angCurrent; if ( m_hViewPositionMover ) { angCurrent = m_hViewPositionMover->GetAbsAngles(); VectorAngles( m_hViewTarget->WorldSpaceCenter() - m_hViewPositionMover->GetAbsOrigin(), angGoal ); } else if ( m_hViewTargetAngles ) { angCurrent = m_hViewTargetAngles->GetAbsAngles(); m_hViewTargetAngles->SetAbsOrigin( pPlayer->EyePosition() ); VectorAngles( m_hViewTarget->WorldSpaceCenter() - m_hViewTargetAngles->GetAbsOrigin(), angGoal ); } else { angCurrent = pPlayer->EyeAngles(); VectorAngles( m_hViewTarget->WorldSpaceCenter() - pPlayer->EyePosition(), angGoal ); } // Accelerate towards the target goal angles float dx = AngleDiff( angGoal.x, angCurrent.x ); float dy = AngleDiff( angGoal.y, angCurrent.y ); float mod = 1.0 - ExponentialDecay( 0.5, 0.3, gpGlobals->frametime ); float dxmod = dx * mod; float dymod = dy * mod; angCurrent.x = AngleNormalize( angCurrent.x + dxmod ); angCurrent.y = AngleNormalize( angCurrent.y + dymod ); if ( m_hViewPositionMover ) { m_hViewPositionMover->SetAbsAngles( angCurrent ); } else if ( m_hViewTargetAngles ) { m_hViewTargetAngles->SetAbsAngles( angCurrent ); pPlayer->SnapEyeAngles( angCurrent ); } else { pPlayer->SnapEyeAngles( angCurrent ); } SetNextThink( gpGlobals->curtime, s_pCommentaryUpdateViewThink ); } if ( m_hViewPosition.Get() ) { if ( pPlayer->GetActiveWeapon() ) { pPlayer->GetActiveWeapon()->Holster(); } if ( !m_hViewPositionMover ) { // Make an invisible info target entity for us to attach the view to, // and move it to the desired view position. m_hViewPositionMover = CreateEntityByName( "point_commentary_viewpoint" ); m_hViewPositionMover->SetAbsAngles( pPlayer->EyeAngles() ); pPlayer->SetViewEntity( m_hViewPositionMover ); } // Blend to the target position over time. float flCurTime = (gpGlobals->curtime - m_flStartTime); float flBlendPerc = clamp( flCurTime / 2.0, 0, 1 ); // Figure out the current view position Vector vecCurEye; VectorLerp( pPlayer->EyePosition(), m_hViewPosition.Get()->GetAbsOrigin(), flBlendPerc, vecCurEye ); m_hViewPositionMover->SetAbsOrigin( vecCurEye ); SetNextThink( gpGlobals->curtime, s_pCommentaryUpdateViewThink ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::UpdateViewPostThink( void ) { CBasePlayer *pPlayer = GetCommentaryPlayer(); if ( !pPlayer ) return; if ( m_hViewPosition.Get() && m_hViewPositionMover ) { // Blend back to the player's position over time. float flCurTime = (gpGlobals->curtime - m_flFinishedTime); float flTimeToBlend = MIN( 2.0, m_flFinishedTime - m_flStartTime ); float flBlendPerc = 1.0 - clamp( flCurTime / flTimeToBlend, 0, 1 ); //Msg("OUT: CurTime %.2f, BlendTime: %.2f, Blend: %.3f\n", flCurTime, flTimeToBlend, flBlendPerc ); // Only do this while we're still moving if ( flBlendPerc > 0 ) { // Figure out the current view position Vector vecPlayerPos = pPlayer->EyePosition(); Vector vecToPosition = (m_vecFinishOrigin - vecPlayerPos); Vector vecCurEye = pPlayer->EyePosition() + (vecToPosition * flBlendPerc); m_hViewPositionMover->SetAbsOrigin( vecCurEye ); if ( m_hViewTarget ) { Quaternion quatFinish; Quaternion quatOriginal; Quaternion quatCurrent; AngleQuaternion( m_vecOriginalAngles, quatOriginal ); AngleQuaternion( m_vecFinishAngles, quatFinish ); QuaternionSlerp( quatFinish, quatOriginal, 1.0 - flBlendPerc, quatCurrent ); QAngle angCurrent; QuaternionAngles( quatCurrent, angCurrent ); m_hViewPositionMover->SetAbsAngles( angCurrent ); } SetNextThink( gpGlobals->curtime, s_pCommentaryUpdateViewThink ); return; } pPlayer->SnapEyeAngles( m_hViewPositionMover->GetAbsAngles() ); } // We're done CleanupPostCommentary(); m_bPreventChangesWhileMoving = false; } //------------------------------------------------------------------------------ // Purpose: //------------------------------------------------------------------------------ void CPointCommentaryNode::FinishCommentary( bool bBlendOut ) { CBasePlayer *pPlayer = GetCommentaryPlayer(); if ( !pPlayer ) return; // Fire off our postcommands if ( m_iszPostCommands != NULL_STRING ) { g_CommentarySystem.SetCommentaryConvarsChanging( true ); engine->ClientCommand( pPlayer->edict(), "%s", STRING(m_iszPostCommands) ); engine->ClientCommand( pPlayer->edict(), "commentary_cvarsnotchanging\n" ); } // Stop the commentary m_flFinishedTime = gpGlobals->curtime; if ( bBlendOut && m_hViewPositionMover ) { m_bActive = false; m_flPlaybackRate = 1.0; m_vecFinishOrigin = m_hViewPositionMover->GetAbsOrigin(); m_vecFinishAngles = m_hViewPositionMover->GetAbsAngles(); m_bPreventChangesWhileMoving = true; // We've moved away from the player's position. Move back to it before ending SetContextThink( &CPointCommentaryNode::UpdateViewPostThink, gpGlobals->curtime, s_pCommentaryUpdateViewThink ); return; } CleanupPostCommentary(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::CleanupPostCommentary( void ) { CBasePlayer *pPlayer = GetCommentaryPlayer(); if ( !pPlayer ) return; if ( ( m_hViewPositionMover || m_hViewTargetAngles ) && pPlayer->GetActiveWeapon() ) { pPlayer->GetActiveWeapon()->Deploy(); } if ( m_hViewTargetAngles && pPlayer->GetViewEntity() == m_hViewTargetAngles ) { pPlayer->SetViewEntity( NULL ); } UTIL_Remove( m_hViewTargetAngles ); if ( m_hViewPositionMover && pPlayer->GetViewEntity() == m_hViewPositionMover ) { pPlayer->SetViewEntity( NULL ); } UTIL_Remove( m_hViewPositionMover ); m_bActive = false; m_flPlaybackRate = 1.0; m_bUnstoppable = false; m_flFinishedTime = 0; SetContextThink( NULL, 0, s_pCommentaryUpdateViewThink ); // Clear out any events waiting on our start commentary g_EventQueue.CancelEvents( this ); m_pOnCommentaryStopped.FireOutput( this, this ); g_CommentarySystem.SetActiveNode( NULL ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::InputStartCommentary( inputdata_t &inputdata ) { if ( !m_bActive ) { if ( g_CommentarySystem.GetActiveNode() ) { g_CommentarySystem.GetActiveNode()->StopPlaying(); } PlayerActivated(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::InputStartUnstoppableCommentary( inputdata_t &inputdata ) { if ( !m_bActive ) { m_bUnstoppable = true; InputStartCommentary( inputdata ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::InputEnable( inputdata_t &inputdata ) { SetDisabled( false ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::InputDisable( inputdata_t &inputdata ) { SetDisabled( true ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::SetDisabled( bool bDisabled ) { m_bDisabled = bDisabled; if ( m_bDisabled ) { AddEffects( EF_NODRAW ); } else { RemoveEffects( EF_NODRAW ); } } //----------------------------------------------------------------------------- // Purpose Force our lighting landmark to be transmitted //----------------------------------------------------------------------------- int CPointCommentaryNode::UpdateTransmitState( void ) { return SetTransmitState( FL_EDICT_ALWAYS ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPointCommentaryNode::SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ) { // Are we already marked for transmission? if ( pInfo->m_pTransmitEdict->Get( entindex() ) ) return; BaseClass::SetTransmit( pInfo, bAlways ); // Force our camera view position entity to be sent if ( m_hViewTarget ) { m_hViewTarget->SetTransmit( pInfo, bAlways ); } if ( m_hViewTargetAngles ) { m_hViewTargetAngles->SetTransmit( pInfo, bAlways ); } if ( m_hViewPosition.Get() ) { m_hViewPosition.Get()->SetTransmit( pInfo, bAlways ); } if ( m_hViewPositionMover ) { m_hViewPositionMover->SetTransmit( pInfo, bAlways ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CPointCommentaryNode::PreventsMovement( void ) { // If we're moving the player's view at all, prevent movement if ( m_hViewPosition.Get() ) return true; return m_bPreventMovement; } //----------------------------------------------------------------------------- // COMMENTARY SAVE / RESTORE //----------------------------------------------------------------------------- static short COMMENTARY_SAVE_RESTORE_VERSION = 2; class CCommentary_SaveRestoreBlockHandler : public CDefSaveRestoreBlockHandler { public: const char *GetBlockName() { return "Commentary"; } //--------------------------------- void Save( ISave *pSave ) { pSave->WriteBool( &g_bInCommentaryMode ); if ( IsInCommentaryMode() ) { pSave->WriteAll( &g_CommentarySystem, g_CommentarySystem.GetDataDescMap() ); pSave->WriteInt( &CAI_BaseNPC::m_nDebugBits ); } } //--------------------------------- void WriteSaveHeaders( ISave *pSave ) { pSave->WriteShort( &COMMENTARY_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. short version; pRestore->ReadShort( &version ); m_fDoLoad = ( version == COMMENTARY_SAVE_RESTORE_VERSION ); } //--------------------------------- void Restore( IRestore *pRestore, bool createPlayers ) { if ( m_fDoLoad ) { pRestore->ReadBool( &g_bInCommentaryMode ); if ( g_bInCommentaryMode ) { pRestore->ReadAll( &g_CommentarySystem, g_CommentarySystem.GetDataDescMap() ); CAI_BaseNPC::m_nDebugBits = pRestore->ReadInt(); } // Force the commentary convar to match the saved game state commentary.SetValue( g_bInCommentaryMode ); } } private: bool m_fDoLoad; }; //----------------------------------------------------------------------------- CCommentary_SaveRestoreBlockHandler g_Commentary_SaveRestoreBlockHandler; //------------------------------------- ISaveRestoreBlockHandler *GetCommentarySaveRestoreBlockHandler() { return &g_Commentary_SaveRestoreBlockHandler; } //----------------------------------------------------------------------------- // Purpose: Commentary specific logic_auto replacement. // Fires outputs based upon how commentary mode has been activated. //----------------------------------------------------------------------------- class CCommentaryAuto : public CBaseEntity { DECLARE_CLASS( CCommentaryAuto, CBaseEntity ); public: DECLARE_DATADESC(); void Spawn(void); void Think(void); void InputMultiplayerSpawned( inputdata_t &inputdata ); private: // fired if commentary started due to new map COutputEvent m_OnCommentaryNewGame; // fired if commentary was turned on in the middle of a map COutputEvent m_OnCommentaryMidGame; // fired when the player spawns in a multiplayer game COutputEvent m_OnCommentaryMultiplayerSpawn; }; LINK_ENTITY_TO_CLASS(commentary_auto, CCommentaryAuto); BEGIN_DATADESC( CCommentaryAuto ) // Inputs DEFINE_INPUTFUNC( FIELD_VOID, "MultiplayerSpawned", InputMultiplayerSpawned ), // Outputs DEFINE_OUTPUT(m_OnCommentaryNewGame, "OnCommentaryNewGame"), DEFINE_OUTPUT(m_OnCommentaryMidGame, "OnCommentaryMidGame"), DEFINE_OUTPUT(m_OnCommentaryMultiplayerSpawn, "OnCommentaryMultiplayerSpawn"), END_DATADESC() //------------------------------------------------------------------------------ // Purpose : Fire my outputs here if I fire on map reload //------------------------------------------------------------------------------ void CCommentaryAuto::Spawn(void) { BaseClass::Spawn(); SetNextThink( gpGlobals->curtime + 0.1 ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CCommentaryAuto::Think(void) { if ( g_CommentarySystem.CommentaryWasEnabledMidGame() ) { m_OnCommentaryMidGame.FireOutput(NULL, this); } else { m_OnCommentaryNewGame.FireOutput(NULL, this); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CCommentaryAuto::InputMultiplayerSpawned( inputdata_t &inputdata ) { m_OnCommentaryMultiplayerSpawn.FireOutput( NULL, this ); }