text: //========= Copyright Valve Corporation, All rights reserved. ============//
  //
  // Purpose: The system for handling director's commentary style production info in-game.
  //
  // $NoKeywords: $
  //=============================================================================//
  
  #include "cbase.h"
  
  #ifndef _XBOX
  #include "tier0/icommandline.h"
  #include "igamesystem.h"
  #include "filesystem.h"
  #include <KeyValues.h>
  #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"
  
  // 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." );
  
  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;
  }
  
  //-----------------------------------------------------------------------------
  // 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();
  
  	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;
  	}
  
  	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 )
  	{
  		if ( !IsInCommentaryMode() )
  			return;
  
  		// Don't spawn commentary entities when loading a savegame
  		if ( gpGlobals->eLoadType == MapLoad_LoadGame || gpGlobals->eLoadType == MapLoad_Background )
  			return;
  
  		m_bCommentaryEnabledMidGame = false;
  		InitCommentary();
  
  		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<CPointCommentaryNode*>(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) 
  			{
  				if ( buttonsPressed & IN_ATTACK2 )
  				{
  					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 )
  	{
  		// 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
  		CBaseEntity *pSemaphore = CreateEntityByName( "info_target" );
  		pSemaphore->SetName( MAKE_STRING(COMMENTARY_SPAWNED_SEMAPHORE) );
  
  		bool oldLock = engine->LockNetworkStringTables( false );
  
  		// 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 );
  
  			// 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 ( !Q_strncmp( pNodeName, "trackinfo", 9 ) )
  				{
  					pkvNode = pkvNode->GetNextKey();
  					continue;
  				}
  
  				KeyValues *pClassname = pkvNode->FindKey( "classname" );
  				if ( pClassname )
  				{
  					// Use the classname instead
  					pNodeName = pClassname->GetString();
  				}
  
  				// Spawn the commentary entity
  				CBaseEntity *pNode = CreateEntityByName( pNodeName );
  				if ( pNode )
  				{
  					ParseEntKVBlock( pNode, pkvNode );
  					DispatchSpawn( pNode );
  
  					EHANDLE hHandle;
  					hHandle = pNode;
  					m_hSpawnedEntities.AddToTail( hHandle );
  
  					CPointCommentaryNode *pCommNode = dynamic_cast<CPointCommentaryNode*>(pNode);
  					if ( pCommNode )
  					{
  						m_iCommentaryNodeCount++;
  						pCommNode->SetNodeNumber( m_iCommentaryNodeCount );
  					}
  				}
  				else
  				{
  					Warning("Commentary: Failed to spawn commentary entity, type: '%s'\n", pNodeName );
  				}
  
  				// Move to next entity
  				pkvNode = pkvNode->GetNextKey();
  			}
  
  			// 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 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();
  
  		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<CPointCommentaryNode *>( 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;
  
  	CUtlVector< modifiedconvars_t > m_ModifiedConvars;
  	CUtlVector<EHANDLE>				m_hSpawnedEntities;
  	CHandle<CPointCommentaryNode>	m_hCurrentNode;
  	CHandle<CPointCommentaryNode>	m_hActiveCommentaryNode;
  	CHandle<CPointCommentaryNode>	m_hLastCommentaryNode;
  };
  
  CCommentarySystem	g_CommentarySystem;
  
  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", FCVAR_NONE, "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
  //===========================================================================================================
  
  //-----------------------------------------------------------------------------
  // 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_iNodeNumber = 0;
  	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 )
  {
  	gamestats->Event_Commentary();
  
  	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(), 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 * 0.5f, 0.f, 1.f );
  
  		// 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.0f - clamp( flCurTime / flTimeToBlend, 0.f, 1.f );
  
  		//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(), 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 );
  }
  
  #else
  
  bool IsInCommentaryMode( void )
  {
  	return false;
  }
  
  #endif
  