You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1184 lines
28 KiB
1184 lines
28 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
// |
|
//=============================================================================// |
|
// hltvdirector.cpp: implementation of the CHLTVDirector class. |
|
// |
|
////////////////////////////////////////////////////////////////////// |
|
|
|
#include "cbase.h" |
|
#include "hltvdirector.h" |
|
#include "KeyValues.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
////////////////////////////////////////////////////////////////////// |
|
// Construction/Destruction |
|
////////////////////////////////////////////////////////////////////// |
|
|
|
|
|
static ConVar tv_delay( "tv_delay", "30", 0, "SourceTV broadcast delay in seconds", true, 0, true, HLTV_MAX_DELAY ); |
|
static ConVar tv_allow_static_shots( "tv_allow_static_shots", "1", 0, "Auto director uses fixed level cameras for shots" ); |
|
static ConVar tv_allow_camera_man( "tv_allow_camera_man", "1", 0, "Auto director allows spectators to become camera man" ); |
|
|
|
static bool GameEventLessFunc( CHLTVGameEvent const &e1, CHLTVGameEvent const &e2 ) |
|
{ |
|
return e1.m_Tick < e2.m_Tick; |
|
} |
|
|
|
#define RANDOM_MAX_ELEMENTS 256 |
|
static int s_RndOrder[RANDOM_MAX_ELEMENTS]; |
|
static void InitRandomOrder(int nFields) |
|
{ |
|
if ( nFields > RANDOM_MAX_ELEMENTS ) |
|
{ |
|
Assert( nFields > RANDOM_MAX_ELEMENTS ); |
|
nFields = RANDOM_MAX_ELEMENTS; |
|
} |
|
|
|
for ( int i=0; i<nFields; i++ ) |
|
{ |
|
s_RndOrder[i]=i; |
|
} |
|
|
|
for ( int i=0; i<(nFields/2); i++ ) |
|
{ |
|
int pos1 = RandomInt( 0, nFields-1 ); |
|
int pos2 = RandomInt( 0, nFields-1 ); |
|
int temp = s_RndOrder[pos1]; |
|
s_RndOrder[pos1] = s_RndOrder[pos2]; |
|
s_RndOrder[pos2] = temp; |
|
} |
|
}; |
|
|
|
|
|
static float WeightedAngle( Vector vec1, Vector vec2) |
|
{ |
|
VectorNormalize( vec1 ); |
|
VectorNormalize( vec2 ); |
|
|
|
float a = DotProduct( vec1, vec2 ); // a = [-1,1] |
|
|
|
a = (a + 1.0f) / 2.0f; |
|
|
|
Assert ( a <= 1 && a >= 0 ); |
|
|
|
return a*a; // vectors are facing opposite direction |
|
} |
|
|
|
#if !defined( CSTRIKE_DLL ) && !defined( DOD_DLL ) && !defined( TF_DLL )// add your mod here if you use your own director |
|
|
|
static CHLTVDirector s_HLTVDirector; // singleton |
|
|
|
EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CHLTVDirector, IHLTVDirector, INTERFACEVERSION_HLTVDIRECTOR, s_HLTVDirector ); |
|
|
|
CHLTVDirector* HLTVDirector() |
|
{ |
|
return &s_HLTVDirector; |
|
} |
|
|
|
IGameSystem* HLTVDirectorSystem() |
|
{ |
|
return &s_HLTVDirector; |
|
} |
|
|
|
#endif // MODs |
|
|
|
|
|
|
|
CHLTVDirector::CHLTVDirector() |
|
{ |
|
m_iPVSEntity = 0; |
|
m_fDelay = 30.0; |
|
m_iLastPlayer = 1; |
|
m_pHLTVServer = NULL; |
|
m_pHLTVClient = NULL; |
|
m_iCameraMan = 0; |
|
m_nNumFixedCameras = 0; |
|
m_EventHistory.SetLessFunc( GameEventLessFunc ); |
|
m_nNextAnalyzeTick = 0; |
|
m_iCameraManIndex = 0; |
|
} |
|
|
|
CHLTVDirector::~CHLTVDirector() |
|
{ |
|
|
|
} |
|
|
|
bool CHLTVDirector::Init() |
|
{ |
|
return gameeventmanager->LoadEventsFromFile( "resource/hltvevents.res" ) > 0; |
|
} |
|
|
|
void CHLTVDirector::Shutdown() |
|
{ |
|
RemoveEventsFromHistory(-1); // all |
|
} |
|
|
|
void CHLTVDirector::FireGameEvent( IGameEvent * event ) |
|
{ |
|
if ( !m_pHLTVServer ) |
|
return; // don't do anything |
|
|
|
CHLTVGameEvent gameevent; |
|
|
|
gameevent.m_Event = gameeventmanager->DuplicateEvent( event ); |
|
gameevent.m_Priority = event->GetInt( "priority", -1 ); // priorities are leveled between 0..10, -1 means ignore |
|
gameevent.m_Tick = gpGlobals->tickcount; |
|
|
|
m_EventHistory.Insert( gameevent ); |
|
} |
|
|
|
IHLTVServer* CHLTVDirector::GetHLTVServer( void ) |
|
{ |
|
return m_pHLTVServer; |
|
} |
|
|
|
void CHLTVDirector::SetHLTVServer( IHLTVServer *hltv ) |
|
{ |
|
RemoveEventsFromHistory(-1); // all |
|
|
|
if ( hltv ) |
|
{ |
|
m_pHLTVClient = UTIL_PlayerByIndex( hltv->GetHLTVSlot() + 1 ); |
|
|
|
if ( m_pHLTVClient && m_pHLTVClient->IsHLTV() ) |
|
{ |
|
m_pHLTVServer = hltv; |
|
} |
|
else |
|
{ |
|
m_pHLTVServer = NULL; |
|
Error( "Couldn't find HLTV client player." ); |
|
} |
|
|
|
// register for events the director needs to know |
|
ListenForGameEvent( "player_hurt" ); |
|
ListenForGameEvent( "player_death" ); |
|
ListenForGameEvent( "round_end" ); |
|
ListenForGameEvent( "round_start" ); |
|
ListenForGameEvent( "hltv_cameraman" ); |
|
ListenForGameEvent( "hltv_rank_entity" ); |
|
ListenForGameEvent( "hltv_rank_camera" ); |
|
} |
|
else |
|
{ |
|
// deactivate HLTV director |
|
m_pHLTVServer = NULL; |
|
} |
|
} |
|
|
|
bool CHLTVDirector::IsActive( void ) |
|
{ |
|
return (m_pHLTVServer != NULL ); |
|
} |
|
|
|
float CHLTVDirector::GetDelay( void ) |
|
{ |
|
return m_fDelay; |
|
} |
|
|
|
int CHLTVDirector::GetDirectorTick( void ) |
|
{ |
|
// just simple delay it |
|
return m_nBroadcastTick; |
|
} |
|
|
|
int CHLTVDirector::GetPVSEntity( void ) |
|
{ |
|
return m_iPVSEntity; |
|
} |
|
|
|
Vector CHLTVDirector::GetPVSOrigin( void ) |
|
{ |
|
return m_vPVSOrigin; |
|
} |
|
|
|
void CHLTVDirector::UpdateSettings() |
|
{ |
|
// set delay |
|
m_fDelay = tv_delay.GetFloat(); |
|
|
|
int newBroadcastTick = gpGlobals->tickcount; |
|
|
|
if ( m_fDelay < HLTV_MIN_DIRECTOR_DELAY ) |
|
{ |
|
// instant broadcast, no delay |
|
m_fDelay = 0.0; |
|
} |
|
else |
|
{ |
|
// broadcast time is current time - delay time |
|
newBroadcastTick -= TIME_TO_TICKS( m_fDelay ); |
|
} |
|
|
|
if( (m_nBroadcastTick == 0) && (newBroadcastTick > 0) ) |
|
{ |
|
// we start broadcasting right now, reset NextShotTimer |
|
m_nNextShotTick = 0; |
|
} |
|
|
|
// check if camera man is still valid |
|
if ( m_iCameraManIndex > 0 ) |
|
{ |
|
CBasePlayer *pPlayer = UTIL_PlayerByIndex( m_iCameraManIndex ); |
|
if ( !pPlayer || pPlayer->GetTeamNumber() != TEAM_SPECTATOR ) |
|
{ |
|
SetCameraMan( 0 ); |
|
} |
|
} |
|
|
|
m_nBroadcastTick = MAX( 0, newBroadcastTick ); |
|
} |
|
|
|
const char** CHLTVDirector::GetModEvents() |
|
{ |
|
static const char *s_modevents[] = |
|
{ |
|
"hltv_status", |
|
"hltv_chat", |
|
"player_connect", |
|
"player_disconnect", |
|
"player_team", |
|
"player_info", |
|
"server_cvar", |
|
"player_death", |
|
"player_chat", |
|
"round_start", |
|
"round_end", |
|
NULL |
|
}; |
|
|
|
return s_modevents; |
|
} |
|
|
|
|
|
void CHLTVDirector::BuildCameraList( void ) |
|
{ |
|
m_nNumFixedCameras = 0; |
|
memset( m_pFixedCameras, 0, sizeof ( m_pFixedCameras ) ); |
|
|
|
CBaseEntity *pCamera = gEntList.FindEntityByClassname( NULL, GetFixedCameraEntityName() ); |
|
|
|
while ( pCamera && m_nNumFixedCameras < MAX_NUM_CAMERAS) |
|
{ |
|
CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, STRING(pCamera->m_target) ); |
|
|
|
if ( pTarget ) |
|
{ |
|
// look at target if any given |
|
QAngle angles; |
|
VectorAngles( pTarget->GetAbsOrigin() - pCamera->GetAbsOrigin(), angles ); |
|
pCamera->SetAbsAngles( angles ); |
|
} |
|
|
|
m_pFixedCameras[m_nNumFixedCameras] = pCamera; |
|
|
|
m_nNumFixedCameras++; |
|
pCamera = gEntList.FindEntityByClassname( pCamera, GetFixedCameraEntityName() ); |
|
} |
|
} |
|
|
|
// this is called with every new map |
|
void CHLTVDirector::LevelInitPostEntity( void ) |
|
{ |
|
BuildCameraList(); |
|
|
|
m_vPVSOrigin.Init(); |
|
m_iPVSEntity = 0; |
|
m_nNextShotTick = 0; |
|
m_nNextAnalyzeTick = 0; |
|
m_iCameraManIndex = 0; |
|
|
|
RemoveEventsFromHistory(-1); // all |
|
|
|
// DevMsg("HLTV Director: found %i fixed cameras.\n", m_nNumFixedCameras ); |
|
} |
|
|
|
void CHLTVDirector::FrameUpdatePostEntityThink( void ) |
|
{ |
|
if ( !m_pHLTVServer ) |
|
return; // don't do anything |
|
|
|
// This function is called each tick |
|
UpdateSettings(); // update settings from cvars |
|
|
|
if ( (m_nNextAnalyzeTick < gpGlobals->tickcount) && |
|
(m_fDelay >= HLTV_MIN_DIRECTOR_DELAY) ) |
|
{ |
|
m_nNextAnalyzeTick = gpGlobals->tickcount + TIME_TO_TICKS( 0.5f ); |
|
|
|
AnalyzePlayers(); |
|
|
|
AnalyzeCameras(); |
|
} |
|
|
|
if ( m_nBroadcastTick <= 0 ) |
|
{ |
|
// game start is still in delay loop |
|
StartDelayMessage(); |
|
} |
|
else if ( m_nNextShotTick <= m_nBroadcastTick ) |
|
{ |
|
// game is being broadcasted, generate camera shots |
|
StartNewShot(); |
|
} |
|
} |
|
|
|
void CHLTVDirector::StartDelayMessage() |
|
{ |
|
if ( m_nNextShotTick > gpGlobals->tickcount ) |
|
return; |
|
|
|
// check the next 8 seconds for interrupts/important events |
|
m_nNextShotTick = gpGlobals->tickcount + TIME_TO_TICKS( DEF_SHOT_LENGTH ); |
|
|
|
// game hasn't started yet, we are still in the broadcast delay hole |
|
IGameEvent *msg = gameeventmanager->CreateEvent( "hltv_message", true ); |
|
|
|
if ( msg ) |
|
{ |
|
msg->SetString("text", "Please wait for broadcast to start ..." ); |
|
|
|
// send spectators the HLTV director command as a game event |
|
m_pHLTVServer->BroadcastEvent( msg ); |
|
gameeventmanager->FreeEvent( msg ); |
|
} |
|
|
|
StartBestFixedCameraShot( true ); |
|
} |
|
|
|
void CHLTVDirector::StartBestPlayerCameraShot() |
|
{ |
|
float flPlayerRanking[MAX_PLAYERS]; |
|
|
|
memset( flPlayerRanking, 0, sizeof(flPlayerRanking) ); |
|
|
|
int firstIndex = FindFirstEvent( m_nBroadcastTick ); |
|
|
|
int index = firstIndex; |
|
|
|
float flBestRank = -1.0f; |
|
int iBestCamera = -1; |
|
int iBestTarget = -1; |
|
|
|
// sum all ranking values for the cameras |
|
|
|
while( index != m_EventHistory.InvalidIndex() ) |
|
{ |
|
CHLTVGameEvent &dc = m_EventHistory[index]; |
|
|
|
if ( dc.m_Tick >= m_nNextShotTick ) |
|
break; |
|
|
|
// search for camera ranking events |
|
if ( Q_strcmp( dc.m_Event->GetName(), "hltv_rank_entity") == 0 ) |
|
{ |
|
int index = dc.m_Event->GetInt("index"); |
|
|
|
if ( index < MAX_PLAYERS ) |
|
{ |
|
flPlayerRanking[index] += dc.m_Event->GetFloat("rank" ); |
|
|
|
// find best camera |
|
if ( flPlayerRanking[index] > flBestRank ) |
|
{ |
|
iBestCamera = index; |
|
flBestRank = flPlayerRanking[index]; |
|
iBestTarget = dc.m_Event->GetInt("target"); |
|
} |
|
} |
|
} |
|
|
|
index = m_EventHistory.NextInorder( index ); |
|
} |
|
|
|
if ( iBestCamera != -1 ) |
|
{ |
|
// view over shoulder, randomly left or right |
|
StartChaseCameraShot( iBestCamera, iBestTarget, 112.0f, 20, (RandomFloat()>0.5)?20:-20, false ); |
|
} |
|
else |
|
{ |
|
StartBestFixedCameraShot( true ); |
|
} |
|
} |
|
|
|
void CHLTVDirector::StartFixedCameraShot(int iCamera, int iTarget) |
|
{ |
|
CBaseEntity *pCamera = m_pFixedCameras[iCamera]; |
|
|
|
|
|
Vector vCamPos = pCamera->GetAbsOrigin(); |
|
QAngle aViewAngle = pCamera->GetAbsAngles(); |
|
|
|
m_iPVSEntity = 0; // don't use camera entity, since it may not been transmitted |
|
m_vPVSOrigin = vCamPos; |
|
|
|
IGameEvent *shot = gameeventmanager->CreateEvent( "hltv_fixed", true ); |
|
|
|
if ( shot ) |
|
{ |
|
shot->SetInt("posx", vCamPos.x ); |
|
shot->SetInt("posy", vCamPos.y ); |
|
shot->SetInt("posz", vCamPos.z ); |
|
shot->SetInt("theta", aViewAngle.x ); |
|
shot->SetInt("phi", aViewAngle.y ); |
|
shot->SetInt("target", iTarget ); |
|
shot->SetFloat("fov", RandomFloat(50,110) ); |
|
|
|
// send spectators the HLTV director command as a game event |
|
m_pHLTVServer->BroadcastEvent( shot ); |
|
gameeventmanager->FreeEvent( shot ); |
|
} |
|
} |
|
|
|
void CHLTVDirector::StartChaseCameraShot(int iTarget1, int iTarget2, int distance, int phi, int theta, bool bInEye) |
|
{ |
|
IGameEvent *shot = gameeventmanager->CreateEvent( "hltv_chase", true ); |
|
|
|
if ( !shot ) |
|
return; |
|
|
|
shot->SetInt("target1", iTarget1 ); |
|
shot->SetInt("target2", iTarget2 ); |
|
shot->SetInt("distance", distance ); |
|
shot->SetInt("phi", phi ); // hi/low |
|
shot->SetInt( "theta", theta ); // left/right |
|
shot->SetInt( "ineye", bInEye?1:0 ); |
|
|
|
m_iPVSEntity = iTarget1; |
|
|
|
// send spectators the HLTV director command as a game event |
|
m_pHLTVServer->BroadcastEvent( shot ); |
|
gameeventmanager->FreeEvent( shot ); |
|
} |
|
|
|
void CHLTVDirector::StartBestFixedCameraShot( bool bForce ) |
|
{ |
|
float flCameraRanking[MAX_NUM_CAMERAS]; |
|
|
|
if ( m_nNumFixedCameras <= 0 ) |
|
return; |
|
|
|
memset( flCameraRanking, 0, sizeof(flCameraRanking) ); |
|
|
|
int firstIndex = FindFirstEvent( m_nBroadcastTick ); |
|
|
|
int index = firstIndex; |
|
|
|
float flBestRank = -1.0f; |
|
int iBestCamera = -1; |
|
int iBestTarget = -1; |
|
|
|
// sum all ranking values for the cameras |
|
|
|
while( index != m_EventHistory.InvalidIndex() ) |
|
{ |
|
CHLTVGameEvent &dc = m_EventHistory[index]; |
|
|
|
if ( dc.m_Tick >= m_nNextShotTick ) |
|
break; |
|
|
|
// search for camera ranking events |
|
if ( Q_strcmp( dc.m_Event->GetName(), "hltv_rank_camera") == 0 ) |
|
{ |
|
int index = dc.m_Event->GetInt("index"); |
|
flCameraRanking[index] += dc.m_Event->GetFloat("rank" ); |
|
|
|
// find best camera |
|
if ( flCameraRanking[index] > flBestRank ) |
|
{ |
|
iBestCamera = index; |
|
flBestRank = flCameraRanking[index]; |
|
iBestTarget = dc.m_Event->GetInt("target"); |
|
} |
|
} |
|
|
|
index = m_EventHistory.NextInorder( index ); |
|
} |
|
|
|
if ( !bForce && flBestRank == 0 ) |
|
{ |
|
// if we are not forcing a fixed camera shot, switch to player chase came |
|
// if no camera shows any players |
|
StartBestPlayerCameraShot(); |
|
} |
|
else if ( iBestCamera != -1 ) |
|
{ |
|
StartFixedCameraShot( iBestCamera, iBestTarget ); |
|
} |
|
} |
|
|
|
void CHLTVDirector::StartRandomShot() |
|
{ |
|
int toTick = m_nBroadcastTick + TIME_TO_TICKS ( DEF_SHOT_LENGTH ); |
|
m_nNextShotTick = MIN( m_nNextShotTick, toTick ); |
|
|
|
if ( RandomFloat(0,1) < 0.25 && tv_allow_static_shots.GetBool() ) |
|
{ |
|
// create a static shot from a level camera |
|
StartBestFixedCameraShot( false ); |
|
} |
|
else |
|
{ |
|
// follow a player |
|
StartBestPlayerCameraShot(); |
|
} |
|
} |
|
|
|
void CHLTVDirector::CreateShotFromEvent( CHLTVGameEvent *event ) |
|
{ |
|
// show event at least for 2 more seconds after it occured |
|
const char *name = event->m_Event->GetName(); |
|
|
|
bool bPlayerHurt = Q_strcmp( "player_hurt", name ) == 0; |
|
bool bPlayerKilled = Q_strcmp( "player_death", name ) == 0; |
|
bool bRoundStart = Q_strcmp( "round_start", name ) == 0; |
|
bool bRoundEnd = Q_strcmp( "round_end", name ) == 0; |
|
|
|
if ( bPlayerHurt || bPlayerKilled ) |
|
{ |
|
CBaseEntity *victim = UTIL_PlayerByUserId( event->m_Event->GetInt("userid") ); |
|
CBaseEntity *attacker = UTIL_PlayerByUserId( event->m_Event->GetInt("attacker") ); |
|
|
|
if ( !victim ) |
|
return; |
|
|
|
if ( attacker == victim || attacker == NULL ) |
|
{ |
|
// player killed self or by WORLD |
|
StartChaseCameraShot( victim->entindex(), 0, 96, 20, 0, false ); |
|
} |
|
else // attacker != NULL |
|
{ |
|
// check if we would show it from ineye view |
|
bool bInEye = (bPlayerKilled && RandomFloat(0,1) > 0.33) || (bPlayerHurt && RandomFloat(0,1) > 0.66); |
|
|
|
// if we show ineye view, show it more likely from killer |
|
if ( RandomFloat(0,1) > (bInEye?0.3f:0.7f) ) |
|
{ |
|
::V_swap( attacker, victim ); |
|
} |
|
|
|
// hurting a victim is shown as chase more often |
|
// view from behind over head |
|
// lower view point, dramatic |
|
// view over shoulder, randomly left or right |
|
StartChaseCameraShot( victim->entindex(), attacker->entindex(), 96, -20, (RandomFloat()>0.5)?30:-30, bInEye ); |
|
} |
|
|
|
// shot 2 seconds after death/hurt |
|
m_nNextShotTick = MIN( m_nNextShotTick, (event->m_Tick+TIME_TO_TICKS(2.0)) ); |
|
} |
|
else if ( bRoundStart || bRoundEnd ) |
|
{ |
|
StartBestFixedCameraShot( false ); |
|
} |
|
else |
|
{ |
|
DevMsg( "No known TV shot for event %s\n", name ); |
|
} |
|
} |
|
|
|
void CHLTVDirector::CheckHistory() |
|
{ |
|
int index = m_EventHistory.FirstInorder(); |
|
int lastTick = -1; |
|
|
|
while ( index != m_EventHistory.InvalidIndex() ) |
|
{ |
|
CHLTVGameEvent &dc = m_EventHistory[index]; |
|
|
|
Assert( lastTick <= dc.m_Tick ); |
|
lastTick = dc.m_Tick; |
|
|
|
index = m_EventHistory.NextInorder( index ); |
|
} |
|
} |
|
|
|
void CHLTVDirector::RemoveEventsFromHistory(int tick) |
|
{ |
|
int index = m_EventHistory.FirstInorder(); |
|
|
|
while ( index != m_EventHistory.InvalidIndex() ) |
|
{ |
|
CHLTVGameEvent &dc = m_EventHistory[index]; |
|
|
|
if ( (dc.m_Tick < tick) || (tick == -1) ) |
|
{ |
|
gameeventmanager->FreeEvent( dc.m_Event ); |
|
dc.m_Event = NULL; |
|
m_EventHistory.RemoveAt( index ); |
|
index = m_EventHistory.FirstInorder(); // start again |
|
} |
|
else |
|
{ |
|
index = m_EventHistory.NextInorder( index ); |
|
} |
|
} |
|
|
|
#ifdef _DEBUG |
|
CheckHistory(); |
|
#endif |
|
} |
|
|
|
int CHLTVDirector::FindFirstEvent( int tick ) |
|
{ |
|
// TODO cache last queried ticks |
|
|
|
int index = m_EventHistory.FirstInorder(); |
|
|
|
if ( index == m_EventHistory.InvalidIndex() ) |
|
return index; // no commands in list |
|
|
|
CHLTVGameEvent *event = &m_EventHistory[index]; |
|
|
|
while ( event->m_Tick < tick ) |
|
{ |
|
index = m_EventHistory.NextInorder( index ); |
|
|
|
if ( index == m_EventHistory.InvalidIndex() ) |
|
break; |
|
|
|
event = &m_EventHistory[index]; |
|
} |
|
|
|
return index; |
|
} |
|
|
|
bool CHLTVDirector::SetCameraMan( int iPlayerIndex ) |
|
{ |
|
if ( !tv_allow_camera_man.GetBool() ) |
|
return false; |
|
|
|
if ( m_iCameraManIndex == iPlayerIndex ) |
|
return true; |
|
|
|
// check if somebody else is already the camera man |
|
if ( m_iCameraManIndex != 0 && iPlayerIndex != 0 ) |
|
return false; |
|
|
|
CBasePlayer *pPlayer = NULL; |
|
|
|
if ( iPlayerIndex > 0 ) |
|
{ |
|
pPlayer = UTIL_PlayerByIndex( iPlayerIndex ); |
|
if ( !pPlayer || pPlayer->GetTeamNumber() != TEAM_SPECTATOR ) |
|
return false; |
|
} |
|
|
|
m_iCameraManIndex = iPlayerIndex; |
|
|
|
// create event for director event history |
|
IGameEvent *event = gameeventmanager->CreateEvent( "hltv_cameraman" ); |
|
if ( event ) |
|
{ |
|
event->SetInt("index", iPlayerIndex ); |
|
gameeventmanager->FireEvent( event ); |
|
} |
|
|
|
CRecipientFilter filter; |
|
|
|
for ( int i = 1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); |
|
|
|
if ( pPlayer && pPlayer->GetTeamNumber() == TEAM_SPECTATOR && !pPlayer->IsFakeClient() ) |
|
{ |
|
filter.AddRecipient( pPlayer ); |
|
} |
|
} |
|
|
|
filter.MakeReliable(); |
|
|
|
if ( iPlayerIndex > 0 ) |
|
{ |
|
// tell all spectators that the camera is in use. |
|
char szText[200]; |
|
Q_snprintf( szText, sizeof(szText), "SourceTV camera is now controlled by %s.", pPlayer->GetPlayerName() ); |
|
UTIL_ClientPrintFilter( filter, HUD_PRINTTALK, szText ); |
|
} |
|
else |
|
{ |
|
// tell all spectators that the camera is available again. |
|
UTIL_ClientPrintFilter( filter, HUD_PRINTTALK, "SourceTV camera switched to auto-director mode." ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
void CHLTVDirector::FinishCameraManShot() |
|
{ |
|
Assert( m_iCameraMan == m_iPVSEntity ); |
|
|
|
int index = FindFirstEvent( m_nBroadcastTick ); |
|
|
|
if ( index == m_EventHistory.InvalidIndex() ) |
|
{ |
|
// check next frame again if event history is empty |
|
m_nNextShotTick = m_nBroadcastTick+1; |
|
return; |
|
} |
|
|
|
m_nNextShotTick = m_nBroadcastTick + TIME_TO_TICKS( MIN_SHOT_LENGTH ); |
|
|
|
//check if camera turns camera off within broadcast time and game time |
|
while( index != m_EventHistory.InvalidIndex() ) |
|
{ |
|
CHLTVGameEvent &dc = m_EventHistory[index]; |
|
|
|
if ( dc.m_Tick >= m_nNextShotTick ) |
|
break; |
|
|
|
if ( Q_strcmp( dc.m_Event->GetName(), "hltv_cameraman") == 0 ) |
|
{ |
|
int iNewCameraMan = dc.m_Event->GetInt("index"); |
|
|
|
if ( iNewCameraMan == 0 ) |
|
{ |
|
// camera man switched camera off |
|
m_nNextShotTick = dc.m_Tick+1; |
|
m_iCameraMan = 0; |
|
return; |
|
} |
|
} |
|
|
|
index = m_EventHistory.NextInorder( index ); |
|
} |
|
|
|
// camera man is still recording and live, resend camera man message |
|
IGameEvent *msg = gameeventmanager->CreateEvent( "hltv_cameraman", true ); |
|
if ( msg ) |
|
{ |
|
msg->SetInt("index", m_iCameraMan ); |
|
m_pHLTVServer->BroadcastEvent( msg ); |
|
gameeventmanager->FreeEvent( msg ); |
|
} |
|
|
|
} |
|
|
|
bool CHLTVDirector::StartCameraManShot() |
|
{ |
|
Assert( m_nNextShotTick <= m_nBroadcastTick ); |
|
|
|
int index = FindFirstEvent( m_nNextShotTick ); |
|
|
|
// check for cameraman mode |
|
while( index != m_EventHistory.InvalidIndex() ) |
|
{ |
|
CHLTVGameEvent &dc = m_EventHistory[index]; |
|
|
|
// only check if this is the current tick |
|
if ( dc.m_Tick > m_nBroadcastTick ) |
|
break; |
|
|
|
if ( Q_strcmp( dc.m_Event->GetName(), "hltv_cameraman") == 0 ) |
|
{ |
|
if ( dc.m_Event->GetInt("index") > 0 ) |
|
{ |
|
// ok, this guy is now the active camera man |
|
m_iCameraMan = dc.m_Event->GetInt("index"); |
|
|
|
m_iPVSEntity = m_iCameraMan; |
|
m_nNextShotTick = m_nBroadcastTick+1; // check setting right on next frame |
|
|
|
// send camera man command to client |
|
m_pHLTVServer->BroadcastEvent( dc.m_Event ); |
|
return true; |
|
} |
|
} |
|
|
|
index = m_EventHistory.NextInorder( index ); |
|
} |
|
|
|
return false; // no camera man found |
|
} |
|
|
|
void CHLTVDirector::StartInstantBroadcastShot() |
|
{ |
|
m_nNextShotTick = m_nBroadcastTick + TIME_TO_TICKS( MAX_SHOT_LENGTH ); |
|
|
|
if ( m_iCameraManIndex > 0 ) |
|
{ |
|
// camera man is still recording and live, resend camera man message |
|
IGameEvent *msg = gameeventmanager->CreateEvent( "hltv_cameraman", true ); |
|
if ( msg ) |
|
{ |
|
msg->SetInt("index", m_iCameraManIndex ); |
|
m_pHLTVServer->BroadcastEvent( msg ); |
|
gameeventmanager->FreeEvent( msg ); |
|
|
|
m_iPVSEntity = m_iCameraManIndex; |
|
m_nNextShotTick = m_nBroadcastTick+TIME_TO_TICKS( MIN_SHOT_LENGTH ); |
|
} |
|
} |
|
else |
|
{ |
|
RemoveEventsFromHistory(-1); // all |
|
|
|
AnalyzePlayers(); |
|
|
|
AnalyzeCameras(); |
|
|
|
StartRandomShot(); |
|
} |
|
} |
|
|
|
void CHLTVDirector::StartNewShot() |
|
{ |
|
// we can remove all events the |
|
int smallestTick = MAX(0, gpGlobals->tickcount - TIME_TO_TICKS(HLTV_MAX_DELAY) ); |
|
RemoveEventsFromHistory( smallestTick ); |
|
|
|
// if the delay time is to short for autodirector, just show next best thing |
|
if ( m_fDelay < HLTV_MIN_DIRECTOR_DELAY ) |
|
{ |
|
StartInstantBroadcastShot(); |
|
return; |
|
} |
|
|
|
if ( m_iCameraMan > 0 ) |
|
{ |
|
// we already have an active camera man, |
|
// wait until he releases the "record" lock |
|
FinishCameraManShot(); |
|
return; |
|
} |
|
|
|
if ( StartCameraManShot() ) |
|
{ |
|
// now we have an active camera man |
|
return; |
|
} |
|
|
|
// ok, no camera man active, now check how much time |
|
// we have for the next shot, if the time diff to the next |
|
// important event we have to switch to is too short (<2sec) |
|
// just extent the current shot and don't start a new one |
|
|
|
// check the next 8 seconds for interrupts/important events |
|
m_nNextShotTick = m_nBroadcastTick + TIME_TO_TICKS( MAX_SHOT_LENGTH ); |
|
|
|
if ( m_nBroadcastTick <= 0 ) |
|
{ |
|
// game hasn't started yet, we are still in the broadcast delay hole |
|
IGameEvent *msg = gameeventmanager->CreateEvent( "hltv_message", true ); |
|
|
|
if ( msg ) |
|
{ |
|
msg->SetString("text", "Please wait for broadcast to start ..." ); |
|
|
|
// send spectators the HLTV director command as a game event |
|
m_pHLTVServer->BroadcastEvent( msg ); |
|
gameeventmanager->FreeEvent( msg ); |
|
} |
|
|
|
StartBestFixedCameraShot( true ); |
|
return; |
|
} |
|
|
|
int index = FindFirstEvent( m_nBroadcastTick ); |
|
|
|
while( index != m_EventHistory.InvalidIndex() ) |
|
{ |
|
CHLTVGameEvent &dc = m_EventHistory[index]; |
|
|
|
if ( dc.m_Tick >= m_nNextShotTick ) |
|
break; // we have searched enough |
|
|
|
// a camera man is always interrupting auto director |
|
if ( Q_strcmp( dc.m_Event->GetName(), "hltv_cameraman") == 0 ) |
|
{ |
|
if ( dc.m_Event->GetInt("index") > 0 ) |
|
{ |
|
// stop the next cut when this cameraman starts recording |
|
m_nNextShotTick = dc.m_Tick; |
|
break; |
|
} |
|
} |
|
|
|
index = m_EventHistory.NextInorder( index ); |
|
} |
|
|
|
float flDuration = TICKS_TO_TIME(m_nNextShotTick - m_nBroadcastTick); |
|
|
|
if ( flDuration < MIN_SHOT_LENGTH ) |
|
return; // not enough time for a new shot |
|
|
|
// find the most interesting game event for next shot |
|
CHLTVGameEvent *dc = FindBestGameEvent(); |
|
|
|
if ( dc ) |
|
{ |
|
// show the game event |
|
CreateShotFromEvent( dc ); |
|
} |
|
else |
|
{ |
|
// no interesting events found, start random shot |
|
StartRandomShot(); |
|
} |
|
} |
|
|
|
CHLTVGameEvent *CHLTVDirector::FindBestGameEvent() |
|
{ |
|
int bestEvent[4]; |
|
int bestEventPrio[4]; |
|
|
|
Q_memset( bestEvent, 0, sizeof(bestEvent) ); |
|
Q_memset( bestEventPrio, 0, sizeof(bestEventPrio) ); |
|
|
|
int index = FindFirstEvent( m_nBroadcastTick ); |
|
|
|
// search for next 4 best events within next 8 seconds |
|
for (int i = 0; i<4; i ++) |
|
{ |
|
bestEventPrio[i] = 0; |
|
bestEvent[i] = 0; |
|
|
|
int tillTick = m_nBroadcastTick + TIME_TO_TICKS( 2.0f*(1.0f+i) ); |
|
|
|
if ( tillTick > m_nNextShotTick ) |
|
break; |
|
|
|
// sum all action for the next time |
|
while ( index != m_EventHistory.InvalidIndex() ) |
|
{ |
|
CHLTVGameEvent &event = m_EventHistory[index]; |
|
|
|
if ( event.m_Tick > tillTick ) |
|
break; |
|
|
|
int priority = event.m_Priority; |
|
|
|
if ( priority > bestEventPrio[i] ) |
|
{ |
|
bestEvent[i] = index; |
|
bestEventPrio[i] = priority; |
|
} |
|
|
|
index = m_EventHistory.NextInorder( index ); |
|
} |
|
} |
|
|
|
if ( !( bestEventPrio[0] || bestEventPrio[1] || bestEventPrio[2] ) ) |
|
return NULL; // no event found at all, give generic algorithm a chance |
|
|
|
// camera cut rules : |
|
|
|
if ( bestEventPrio[1] >= bestEventPrio[0] && |
|
bestEventPrio[1] >= bestEventPrio[2] && |
|
bestEventPrio[1] >= bestEventPrio[3] ) |
|
{ |
|
return &m_EventHistory[ bestEvent[1] ]; // best case |
|
} |
|
else if ( bestEventPrio[0] > bestEventPrio[1] && |
|
bestEventPrio[0] > bestEventPrio[2] ) |
|
{ |
|
return &m_EventHistory[ bestEvent[0] ]; // event 0 is very important |
|
} |
|
else if ( bestEventPrio[2] > bestEventPrio[3] ) |
|
{ |
|
return &m_EventHistory[ bestEvent[2] ]; |
|
} |
|
else |
|
{ |
|
// event 4 is the best but too far away, so show event 1 |
|
if ( bestEvent[0] ) |
|
return &m_EventHistory[ bestEvent[0] ]; |
|
else |
|
return NULL; |
|
} |
|
} |
|
|
|
void CHLTVDirector::AnalyzeCameras() |
|
{ |
|
InitRandomOrder( m_nNumFixedCameras ); |
|
|
|
for ( int i = 0; i<m_nNumFixedCameras; i++ ) |
|
{ |
|
int iCameraIndex = s_RndOrder[i]; |
|
CBaseEntity *pCamera = m_pFixedCameras[ iCameraIndex ]; |
|
|
|
float flRank = 0.0f; |
|
int iClosestPlayer = 0; |
|
float flClosestPlayerDist = 100000.0f; |
|
int nCount = 0; // Number of visible targets |
|
Vector vDistribution; vDistribution.Init(); // distribution of targets |
|
|
|
Vector vCamPos = pCamera->GetAbsOrigin(); |
|
|
|
for ( int j=0; j<m_nNumActivePlayers; j++ ) |
|
{ |
|
CBasePlayer *pPlayer = m_pActivePlayers[j]; |
|
|
|
Vector vPlayerPos = pPlayer->GetAbsOrigin(); |
|
|
|
float dist = VectorLength( vPlayerPos - vCamPos ); |
|
|
|
if ( dist > 1024.0f || dist < 4.0f ) |
|
continue; // too colse or far away |
|
|
|
// check visibility |
|
trace_t tr; |
|
UTIL_TraceLine( vCamPos, pPlayer->GetAbsOrigin(), MASK_SOLID, pPlayer, COLLISION_GROUP_NONE, &tr ); |
|
|
|
if ( tr.fraction < 1.0 ) |
|
continue; // not visible for camera |
|
|
|
nCount++; |
|
|
|
// remember closest player |
|
if ( dist < flClosestPlayerDist ) |
|
{ |
|
iClosestPlayer = pPlayer->entindex(); |
|
flClosestPlayerDist = dist; |
|
} |
|
|
|
Vector v1; AngleVectors( pPlayer->EyeAngles(), &v1 ); |
|
|
|
// check players orientation towards camera |
|
Vector v2 = vCamPos - vPlayerPos; |
|
VectorNormalize( v2 ); |
|
|
|
// player/camera cost function: |
|
flRank += ( 1.0f/sqrt(dist) ) * WeightedAngle( v1, v2 ); |
|
|
|
vDistribution += v2; |
|
} |
|
|
|
if ( nCount > 0 ) |
|
{ |
|
// normalize distribution |
|
flRank *= VectorLength( vDistribution ) / nCount; |
|
} |
|
|
|
IGameEvent *event = gameeventmanager->CreateEvent("hltv_rank_camera"); |
|
|
|
if ( event ) |
|
{ |
|
event->SetFloat("rank", flRank ); |
|
event->SetInt("index", iCameraIndex ); // index in m_pFixedCameras |
|
event->SetInt("target", iClosestPlayer ); // ent index |
|
gameeventmanager->FireEvent( event ); |
|
} |
|
} |
|
} |
|
|
|
void CHLTVDirector::BuildActivePlayerList() |
|
{ |
|
// first build list of all active players |
|
|
|
m_nNumActivePlayers = 0; |
|
|
|
for ( int i =1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); |
|
|
|
if ( !pPlayer ) |
|
continue; |
|
|
|
if ( !pPlayer->IsAlive() ) |
|
continue; |
|
|
|
if ( pPlayer->IsObserver() ) |
|
continue; |
|
|
|
if ( pPlayer->GetTeamNumber() <= TEAM_SPECTATOR ) |
|
continue; |
|
|
|
m_pActivePlayers[m_nNumActivePlayers] = pPlayer; |
|
m_nNumActivePlayers++; |
|
} |
|
} |
|
|
|
void CHLTVDirector::AnalyzePlayers() |
|
{ |
|
// build list of current active players |
|
BuildActivePlayerList(); |
|
|
|
// analyzes every active player |
|
|
|
InitRandomOrder( m_nNumActivePlayers ); |
|
|
|
for ( int i = 0; i<m_nNumActivePlayers; i++ ) |
|
{ |
|
int iPlayerIndex = s_RndOrder[i]; |
|
|
|
CBasePlayer *pPlayer = m_pActivePlayers[ iPlayerIndex ]; |
|
|
|
float flRank = 0.0f; |
|
int iBestFacingPlayer = 0; |
|
float flBestFacingPlayer = 0.0f; |
|
int nCount = 0; // Number of visible targets |
|
Vector vDistribution; vDistribution.Init(); // distribution of targets |
|
|
|
Vector vCamPos = pPlayer->GetAbsOrigin(); |
|
|
|
Vector v1; AngleVectors( pPlayer->EyeAngles(), &v1 ); |
|
|
|
v1 *= -1; // inverted |
|
|
|
for ( int j=0; j<m_nNumActivePlayers; j++ ) |
|
{ |
|
if ( iPlayerIndex == j ) |
|
continue; // don't check against itself |
|
|
|
CBasePlayer *pOtherPlayer = m_pActivePlayers[j]; |
|
|
|
Vector vPlayerPos = pOtherPlayer->GetAbsOrigin(); |
|
|
|
float dist = VectorLength( vPlayerPos - vCamPos ); |
|
|
|
if ( dist > 1024.0f || dist < 4.0f ) |
|
continue; // too close or far away |
|
|
|
// check visibility |
|
trace_t tr; |
|
UTIL_TraceLine( vCamPos, pOtherPlayer->GetAbsOrigin(), MASK_SOLID, pOtherPlayer, COLLISION_GROUP_NONE, &tr ); |
|
|
|
if ( tr.fraction < 1.0 ) |
|
continue; // not visible for camera |
|
|
|
nCount++; |
|
|
|
// check players orientation towards camera |
|
Vector v2; AngleVectors( pOtherPlayer->EyeAngles(), &v2 ); |
|
|
|
float facing = WeightedAngle( v1, v2 ); |
|
|
|
// remember closest player |
|
if ( facing > flBestFacingPlayer ) |
|
{ |
|
iBestFacingPlayer = pOtherPlayer->entindex(); |
|
flBestFacingPlayer = facing; |
|
} |
|
|
|
// player/camera cost function: |
|
flRank += ( 1.0f/sqrt(dist) ) * facing; |
|
|
|
vDistribution += v2; |
|
} |
|
|
|
if ( nCount > 0 ) |
|
{ |
|
float flDistribution = VectorLength( vDistribution ) / nCount; // normalize distribution |
|
flRank *= flDistribution; |
|
} |
|
|
|
IGameEvent *event = gameeventmanager->CreateEvent("hltv_rank_entity"); |
|
if ( event ) |
|
{ |
|
event->SetInt("index", pPlayer->entindex() ); |
|
event->SetFloat("rank", flRank ); |
|
event->SetInt("target", iBestFacingPlayer ); // ent index |
|
gameeventmanager->FireEvent( event ); |
|
} |
|
} |
|
} |
|
|
|
|