//===== Copyright  Valve Corporation, All rights reserved. ======//
//
//  Radial, context-sensitive menu for co-op communication
//
//================================================================//

#include "cbase.h"
#include <string.h>
#include <stdio.h>
#include <vgui_controls/ImagePanel.h>
#include "voice_status.h"
#include "c_playerresource.h"
#include "cliententitylist.h"
#include "c_baseplayer.h"
#include "materialsystem/imesh.h"
#include "view.h"
#include "materialsystem/imaterial.h"
#include "tier0/dbg.h"
#include "cdll_int.h"
#include "menu.h" // for chudmenu defs
#include "keyvalues.h"
#include <filesystem.h>
#include "c_team.h"
#include "vgui/isurface.h"
#include "iclientmode.h"
#include "c_portal_player.h"
#include "hud_locator_target.h"
#include "c_user_message_register.h"
#include "portal_placement.h"
#include "glow_outline_effect.h"
#include "soundemittersystem/isoundemittersystembase.h"
#include "c_prop_portal.h"
#include "c_trigger_tractorbeam.h"
#include "c_projectedwallentity.h"
#include "portal_mp_gamerules.h"

#include "vgui/cursor.h"
#include "fmtstr.h"
#include "vgui_int.h"
#include "vgui/IVgui.h"
#include <game/client/iviewport.h>

#include "radialmenu.h"
#include "radialmenu_taunt.h"
#include "radialbutton.h"

#include "cegclientwrapper.h"

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"

extern void AddLocator( C_BaseEntity *pTarget, const Vector &vPosition, const Vector &vNormal, int nPlayerIndex, const char *caption, float fDisplayTime );

ConVar cl_fastradial( "cl_fastradial", "1", FCVAR_DEVELOPMENTONLY, "If 1, releasing the button on a radial menu executes the highlighted button" );
ConVar RadialMenuDebug( "cl_rosette_debug", "0" );
ConVar cl_rosette_line_inner_radius( "cl_rosette_line_inner_radius", "25" );
ConVar cl_rosette_line_outer_radius( "cl_rosette_line_outer_radius", "45" );
ConVar cl_rosette_gamepad_lockin_time( "cl_rosette_gamepad_lockin_time", "0.2" );
ConVar cl_rosette_gamepad_expand_time( "cl_rosette_gamepad_expand_time", "0.5" );

#if defined( PORTAL2_PUZZLEMAKER )
extern ConVar sv_record_playtest;
#endif // PORTAL2_PUZZLEMAKER


#define PING_DELAY_BASE 0.05f
#define PING_DELAY_INCREMENT 0.05f

#define PING_SOUND_NAME "GameUI.UiCoopHudClick"
#define PING_SOUND_NAME_LOW "GameUI.UiCoopHudClickLow"
#define PING_SOUND_NAME_HIGH "GameUI.UiCoopHudClickHigh"

#define RADIAL_MENU_POINTER_TEXTURE "vgui/hud/icon_arrow_right"


void FlushClientMenus( void );
void CloseRadialMenuCommand( RadialMenuTypes_t menuType, bool bForceClose = false );


static ClientMenuManager TheClientMenuManager;
static ClientMenuManagerPlaytest TheClientMenuManagerPlaytest;


//--------------------------------------------------------------------------------------------------------
static char s_radialMenuName[ MAX_SPLITSCREEN_PLAYERS ][ 64 ];
static bool s_mouseMenuKeyHeld[ MAX_SPLITSCREEN_PLAYERS ];

// Precache our glow effects
PRECACHE_REGISTER_BEGIN( GLOBAL, PrecacheRadialMenu )
	PRECACHE( MATERIAL, "dev/glow_blur_x" )
	PRECACHE( MATERIAL, "dev/glow_blur_y" )
	PRECACHE( MATERIAL, "dev/glow_color" )
	PRECACHE( MATERIAL, "dev/glow_downsample" )
	PRECACHE( MATERIAL, "dev/halo_add_to_screen" )
	PRECACHE( MATERIAL, RADIAL_MENU_POINTER_TEXTURE )
PRECACHE_REGISTER_END()

#define GLOW_OUTLINE_COLOR	Vector( 0.25f, 0.62f, 1.0f )
#define GLOW_OUTLINE_ALPHA	0.75f


struct SignifierInfo_t
{
	EHANDLE hTarget;
	Vector vPos;
	Vector vNormal;
	int nPlayerIndex;
	char szCaption[ 32 ];
	float fDisplayTime;
};

CUtlVector< SignifierInfo_t > g_SignifierQueue[ MAX_SPLITSCREEN_PLAYERS ];

CUtlVector< SignifierInfo_t >* GetSignifierQueue( void )
{
	ASSERT_LOCAL_PLAYER_RESOLVABLE();
	return &(g_SignifierQueue[ GET_ACTIVE_SPLITSCREEN_SLOT() ]);
}

void TeamPingColor( int nTeamNumber, Vector &vColor )
{
	Color color = Color( GLOW_OUTLINE_COLOR[0], GLOW_OUTLINE_COLOR[1], GLOW_OUTLINE_COLOR[2] );
	if ( nTeamNumber == TEAM_RED )
	{
		color = UTIL_Portal_Color( 2, 0 );  //orange
	}
	else
	{
		color = UTIL_Portal_Color( 1, 0 );  //blue
	}

	vColor.x = (float)color.r()/255.0f;
	vColor.y = (float)color.g()/255.0f;
	vColor.z = (float)color.b()/255.0f;
}

//--------------------------------------------------------------------------------------------------------
int AddGlowToObject( C_BaseEntity *pObject, int nTeamNumber )
{
	if ( pObject == NULL || pObject->GetRenderAlpha() <= 0 )
		return -1;

	// Determine if this entity uses the glow capability
	ISignifierTarget *pSignifier = dynamic_cast<ISignifierTarget *>(pObject);
	if ( pSignifier != NULL )
	{
		if ( pSignifier->UseSelectionGlow() == false )
			return -1;
	}

	Vector vColor;
	TeamPingColor( nTeamNumber, vColor );

	return g_GlowObjectManager.RegisterGlowObject( pObject, vColor, GLOW_OUTLINE_ALPHA, GET_ACTIVE_SPLITSCREEN_SLOT() );
}

void RadialMenuMouseCallback( uint8* pData, size_t iSize )
{
	// make sure we're getting as much data as we expect
	Assert( iSize == sizeof(int)*2 );
	// check for null pointer
	if ( pData == NULL )
		return;

	// capture the data
	int nCursorX = ((int*)pData)[0];
	int nCursorY = ((int*)pData)[1];
	// get the radial menu and give it the cursor position
	CRadialMenu *pRadialMenu = GET_HUDELEMENT( CRadialMenu );
	if ( pRadialMenu )
	{
		pRadialMenu->SetDemoCursorPos( nCursorX, nCursorY );
	}
}

void ClientMenuManager::AddMenuFile( const char *filename )
{
	if ( !m_menuKeys )
	{
		m_menuKeys = new KeyValues( "ClientMenu" );
	}

	KeyValues *data = new KeyValues( "ClientMenu" );
	if ( !data->LoadFromFile( filesystem, filename ) )
	{
		Warning( "Could not load client menu %s\n", filename );
		data->deleteThis();
		return;
	}

	KeyValues *menuKey = data->GetFirstTrueSubKey();
	while ( menuKey )
	{
		if ( m_menuKeys->FindKey( menuKey->GetName(), false ) == NULL )
		{
			data->RemoveSubKey( menuKey );
			m_menuKeys->AddSubKey( menuKey );
		}
		menuKey = data->GetFirstTrueSubKey();
	}
	data->deleteThis();
}

KeyValues *ClientMenuManager::FindMenu( const char *menuName )
{
	// do not show the menu if the player is dead or is an observer
	C_BasePlayer *player = C_BasePlayer::GetLocalPlayer();
	if ( !player )
		return NULL;

	// Find our menu, honoring Alive/Dead/Team suffixes
	/*
	const char *teamSuffix = player->GetTeam()->Get_Name();
	const char *lifeSuffix = player->IsAlive() ? "Alive" : "Dead";

	CFmtStr str;
	const char *fullMenuName = str.sprintf( "%s,%s,%s", menuName, teamSuffix, lifeSuffix );
	*/

	CFmtStr str;
	const char *fullMenuName = str.sprintf( "%s,%s", "Command", menuName );
	KeyValues *menuKey = m_menuKeys->FindKey( fullMenuName, false );
	if ( !menuKey )
	{
		fullMenuName = menuName;
		menuKey = m_menuKeys->FindKey( fullMenuName, false );
	}

	return menuKey;
}

void ClientMenuManager::Flush( void )
{
	Reset();
	AddMenuFile( "scripts/RadialMenu.txt" );
}


//-------------------------------------------------------------------------------------------------------
KeyValues *ClientMenuManagerPlaytest::FindMenu( const char *menuName )
{
	return ClientMenuManager::FindMenu( menuName );
}


//-------------------------------------------------------------------------------------------------------
void ClientMenuManagerPlaytest::Flush( void )
{
	Reset();
	AddMenuFile( "scripts/RadialMenuPlaytest.txt" );
}


void ShowRadialMenuPanel( bool bShow )
{
	if ( IsPC() )
	{
		GetViewPortInterface()->ShowPanel( PANEL_RADIAL_MENU, bShow );
	}
}


CRadialMenuPanel::CRadialMenuPanel(IViewPort *pViewPort) : BaseClass(NULL, PANEL_RADIAL_MENU)
{
	m_pViewPort = pViewPort;

	// load the new scheme early!!
	SetScheme("ClientScheme");
	SetMoveable(false);
	SetSizeable(false);

	SetCursor( NULL );

	LoadControlSettings("Resource/UI/RadialMenuPanel.res");
	InvalidateLayout();

	SetPaintBackgroundEnabled( false );
}


void CRadialMenuPanel::ShowPanel( bool bShow )
{
	if ( BaseClass::IsVisible() == bShow )
		return;

	if ( bShow )
	{
		Activate();

		SetMouseInputEnabled( true );
	}
	else
	{
		SetVisible( false );
		SetMouseInputEnabled( false );
	}

	m_pViewPort->ShowBackGround( false );
}



float CRadialMenu::m_fLastPingTime[ MAX_SPLITSCREEN_PLAYERS ][ 2 ] = { { 0.0f, 0.0f }, { 0.0f, 0.0f } };
int CRadialMenu::m_nNumPings[ MAX_SPLITSCREEN_PLAYERS ][ 2 ] = { { 0, 0 }, { 0, 0 } };

DECLARE_HUDELEMENT( CRadialMenu );

//--------------------------------------------------------------------------------------------------------------
CRadialMenu::CRadialMenu( const char *pElementName )
		: CHudElement( pElementName ), BaseClass( NULL, "RadialMenu" )
{
	SetParent( GetClientMode()->GetViewport() );
	SetHiddenBits( HIDEHUD_PLAYERDEAD );

	MEM_ALLOC_CREDIT();
	// initialize dialog

	m_resource = new KeyValues( "RadialMenu" );
	m_resource->LoadFromFile( filesystem, "resource/UI/RadialMenu.res" );
	m_menuData = NULL;
	FlushClientMenus();

	// load the new scheme early!!
	SetScheme("ClientScheme");
	SetProportional(true);

	HandleControlSettings();

	SetCursor( NULL );

	m_nArrowTexture = -1;

	m_minButtonX = 0;
	m_minButtonY = 0;
	m_maxButtonX = 0;
	m_maxButtonY = 0;

	m_demoCursorX = m_demoCursorY = 0;
	
	m_nEntityGlowIndex = -1;

	m_bEditing = false;
	m_bDragging = false;
	m_nDraggingTaunt = -1;

	m_fFadeInTime = 0.0f;
	m_fSelectionLockInTime = 0.0f;

	m_bEnabled = false;
	m_bQuickPingForceClose = false;

	vgui::ivgui()->AddTickSignal( GetVPanel() );
}


//--------------------------------------------------------------------------------------------------------
const char *CRadialMenu::ButtonNameFromDir( ButtonDir dir )
{
	switch( dir )
	{
	case CENTER:
		return "Center";
	case NORTH:
		return "North";
	case NORTH_EAST:
		return "NorthEast";
	case EAST:
		return "East";
	case SOUTH_EAST:
		return "SouthEast";
	case SOUTH:
		return "South";
	case SOUTH_WEST:
		return "SouthWest";
	case WEST:
		return "West";
	case NORTH_WEST:
		return "NorthWest";
	}

	return "None";
}


//--------------------------------------------------------------------------------------------------------
CRadialMenu::ButtonDir CRadialMenu::DirFromButtonName( const char * name )
{
	if ( FStrEq( name, "Center" ) )
		return CENTER;
	if ( FStrEq( name, "North" ) )
		return NORTH;
	if ( FStrEq( name, "NorthEast" ) )
		return NORTH_EAST;
	if ( FStrEq( name, "East" ) )
		return EAST;
	if ( FStrEq( name, "SouthEast" ) )
		return SOUTH_EAST;
	if ( FStrEq( name, "South" ) )
		return SOUTH;
	if ( FStrEq( name, "SouthWest" ) )
		return SOUTH_WEST;
	if ( FStrEq( name, "West" ) )
		return WEST;
	if ( FStrEq( name, "NorthWest" ) )
		return NORTH_WEST;

	return CENTER;
}


//--------------------------------------------------------------------------------------------------------
/**
 * Created controls from the resource file.  We know how to make a special PolygonButton :)
 */
vgui::Panel *CRadialMenu::CreateControlByName( const char *controlName )
{
	if( !Q_stricmp( "PolygonButton", controlName ) )
	{
		vgui::Button *newButton = new CRadialButton( this, NULL );
		return newButton;
	}
	else
	{
		return BaseClass::CreateControlByName( controlName );
	}
}


//--------------------------------------------------------------------------------------------------------
CRadialMenu::~CRadialMenu()
{
	m_resource->deleteThis();
	if ( m_menuData )
	{
		m_menuData->deleteThis();
	}

	if ( vgui::surface() && m_nArrowTexture != -1 )
	{
		vgui::surface()->DestroyTextureID( m_nArrowTexture );
		m_nArrowTexture = -1;
	}
}

void CRadialMenu::HandleControlSettings()
{
	LoadControlSettings("Resource/UI/RadialMenu.res");

	for ( int i=0; i<NUM_BUTTON_DIRS; ++i )
	{
		ButtonDir dir = (ButtonDir)i;
		const char *buttonName = ButtonNameFromDir( dir );
		m_buttons[i] = dynamic_cast<CRadialButton *>(FindChildByName( buttonName ));
		if ( m_buttons[i] )
		{
			m_buttons[i]->SetMouseInputEnabled( true );
		}
	}

	m_armedButtonDir = CENTER;
}

void CRadialMenu::StartDrag( void )
{
	if ( m_nDraggingTaunt == -1 )
	{
		return;
	}

	m_bDragging = true;

	for ( int i=0; i<NUM_BUTTON_DIRS; ++i )
	{
		ButtonDir dir = (ButtonDir)i;
		const char *buttonName = ButtonNameFromDir( dir );
		m_buttons[i] = dynamic_cast<CRadialButton *>(FindChildByName( buttonName ));
		if ( m_buttons[i] )
		{
			m_buttons[i]->SetMouseInputEnabled( false );
		}
	}

	SetArmedButtonDir( CENTER );
}

void CRadialMenu::EndDrag( void )
{
	if ( !m_bDragging || m_nDraggingTaunt == -1 )
	{
		return;
	}

	int nSwap = -1;

	for ( int i=0; i<NUM_BUTTON_DIRS; ++i )
	{
		ButtonDir dir = (ButtonDir)i;
		const char *buttonName = ButtonNameFromDir( dir );
		m_buttons[i] = dynamic_cast<CRadialButton *>(FindChildByName( buttonName ));
		if ( m_buttons[i] )
		{
			m_buttons[i]->SetMouseInputEnabled( true );

			int mouseX, mouseY;
			vgui::surface()->SurfaceGetCursorPos( mouseX, mouseY );
			if ( nSwap == -1 && m_buttons[i]->IsVisible() && m_buttons[i]->IsEnabled() && m_buttons[i]->IsWithinTraverse( mouseX, mouseY, false ) )
			{
				nSwap = i;
			}
		}
	}

	if ( nSwap != -1 && nSwap != CENTER )
	{
		CUtlVector< TauntStatusData > *pTauntData = GetClientMenuManagerTaunt().GetTauntData();
		TauntStatusData *pData = &((*pTauntData)[ m_nDraggingTaunt ]);

		const char *pDir = "empty";

		switch ( nSwap )
		{
		case NORTH:
			pDir = "North";
			break;

		case SOUTH:
			pDir = "South";
			break;

		case WEST:
			pDir = "West";
			break;

		case EAST:
			pDir = "East";
			break;

		case NORTH_WEST:
			pDir = "NorthWest";
			break;

		case NORTH_EAST:
			pDir = "NorthEast";
			break;

		case SOUTH_WEST:
			pDir = "SouthWest";
			break;

		case SOUTH_EAST:
			pDir = "SouthEast";
			break;
		}

		GetClientMenuManagerTaunt().SetTauntPosition( pData->szName, pDir );
		//GetClientMenuManagerTaunt().UpdateStorageChange( pData, GetClientMenuManagerTaunt().UPDATE_STORAGE_EQUIPSLOT );

		KeyValues *menuKey = GetClientMenuManagerTaunt().FindMenu( "Default" );
		if ( menuKey )
		{
			SetData( menuKey );
		}
	}

	m_bDragging = false;
	m_nDraggingTaunt = -1;
}


//--------------------------------------------------------------------------------------------------------
/**
 * The radial menu should cover the entire screen to capture mouse input, so we should have a blank
 * background.
 */
void CRadialMenu::ApplySchemeSettings( vgui::IScheme *pScheme )
{
	HandleControlSettings();

	BaseClass::ApplySchemeSettings( pScheme );
	SetBgColor( Color( 0, 0, 0, 0 ) );
	m_lineColor = pScheme->GetColor( "rosette.LineColor", Color( 192, 192, 192, 128 ) );

	if ( RadialMenuDebug.GetBool() )
	{
		SetCursor( vgui::dc_crosshair );
	}
	else
	{
		SetCursor( NULL );
	}

	// Restore button names/commands
	if ( m_menuData )
	{
		SetData( m_menuData );
	}

	SetAlpha( 0 );
}


void CRadialMenu::PerformLayout( void )
{
	SetRadialMenuEnabled( false );

	BaseClass::PerformLayout();
}


//--------------------------------------------------------------------------------------------------------
void CRadialMenu::ShowPanel( bool show )
{
	if ( IsVisible() == show )
		return;

	if ( show )
	{
		for ( int i=0; i<NUM_BUTTON_DIRS; ++i )
		{
			if ( !m_buttons[i] )
				continue;

			m_buttons[i]->SetArmed( false );
			m_buttons[i]->SetFakeArmed( false );
			m_buttons[i]->SetChosen( false );
		}

		SetMouseInputEnabled( true );
		m_cursorX = m_cursorY = 0;
		m_demoCursorX = m_demoCursorY = 0;
		m_bFirstCentering = true;
	}
	else
	{
		SetRadialMenuEnabled( false );
		SetMouseInputEnabled( false );
	}
}

void CRadialMenu::PaintBackground( void )
{
	int nSlot = vgui::ipanel()->GetMessageContextId( GetVPanel() );
	ACTIVE_SPLITSCREEN_PLAYER_GUARD( nSlot );

	if ( m_armedButtonDir != CENTER || !m_buttons[ CENTER ]->IsVisible() )
	{
		int x, y, wide, tall;
		GetBounds( x, y, wide, tall );

		float fCenterX = x + wide/2;
		float fCenterY = y + tall/2;

		int nCursorX, nCursorY;
		if ( !input->ControllerModeActive() )
		{
			nCursorX = m_cursorX;
			nCursorY = m_cursorY;
		}
		else
		{
			float fJoyForward, fJoySide, fJoyPitch, fJoyYaw = 0.0f;
			input->Joystick_Querry( fJoyForward, fJoySide, fJoyPitch, fJoyYaw );

			// Replace if the other stick was pushed further
			// We need to use both sticks because they might have southpaw or legacy set
			if ( fabsf( fJoyForward ) > fabsf( fJoyPitch ) )
			{
				fJoyPitch = fJoyForward;
			}
			else
			{
				// Reflip it if the y was inverted because this shouldn't be inverted
				static SplitScreenConVarRef s_joy_inverty( "joy_inverty" );
				if ( s_joy_inverty.IsValid() && s_joy_inverty.GetBool( nSlot ) )
				{
					fJoyPitch *= -1.0f;
				}
			}

			// Replace if the other stick was pushed further
			// We need to use both sticks because they might have southpaw or legacy set
			if ( fabsf( fJoySide ) > fabsf( fJoyYaw ) )
			{
				fJoyYaw = fJoySide;
			}

			if ( fJoyPitch > 0.1f || fJoyYaw > 0.1f )
			{
				nCursorX = fCenterX + fJoyYaw * 100.0f;
				nCursorY = fCenterY + fJoyPitch * 100.0f;
			}
			else
			{
				int buttonX, buttonY;
				m_buttons[ m_armedButtonDir ]->GetIcon()->GetPos( nCursorX, nCursorY );
				m_buttons[ m_armedButtonDir ]->GetPos( buttonX, buttonY );
				nCursorX += buttonX + m_buttons[ m_armedButtonDir ]->GetIcon()->GetWide() / 2;
				nCursorY += buttonY + m_buttons[ m_armedButtonDir ]->GetIcon()->GetTall() / 2;
			}
		}

		Vector2D vNormal( nCursorX - fCenterX, nCursorY - fCenterY );
		if ( vNormal.IsZero() )
		{
			vNormal = Vector2D( 0.0f, -1.0f );
		}
		Vector2DNormalize( vNormal );
		Vector2D vDown( vNormal.y, -vNormal.x );

		float innerRadius = cl_rosette_line_inner_radius.GetFloat();
		float outerRadius = cl_rosette_line_outer_radius.GetFloat();
		innerRadius = YRES( innerRadius ) * 0.75f;
		outerRadius = YRES( outerRadius );

		float fSize = ( outerRadius - innerRadius ) * 0.5f;
		float fOffset = innerRadius + fSize;

		fCenterX += vNormal.x * fOffset;
		fCenterY += vNormal.y * fOffset;

		vgui::Vertex_t points[4] =
		{
			vgui::Vertex_t( Vector2D( fCenterX, fCenterY ) - vDown * fSize - vNormal * fSize,	Vector2D(0,0) ),	// Top Left
			vgui::Vertex_t( Vector2D( fCenterX, fCenterY ) + vDown * fSize - vNormal * fSize,	Vector2D(0,1) ),	// Bottom Left
			vgui::Vertex_t( Vector2D( fCenterX, fCenterY ) + vDown * fSize + vNormal * fSize,	Vector2D(1,1) ),	// Bottom Right
			vgui::Vertex_t( Vector2D( fCenterX, fCenterY ) - vDown * fSize + vNormal * fSize,	Vector2D(1,0) )		// Top Right
		};

		vgui::surface()->DrawSetColor( 255, 255, 255, 255 );
		vgui::surface()->DrawSetTexture( m_nArrowTexture );
		vgui::surface()->DrawTexturedPolygon( 4, points );
	}

	BaseClass::PaintBackground();
}

//--------------------------------------------------------------------------------------------------------
void CRadialMenu::Paint( void )
{
	int nBaseAlpha = 0;
	float fFade = gpGlobals->curtime - m_fFadeInTime;
	if ( fFade > 0.0f )
	{
		fFade = MIN( 1.0f, fFade * 10.0f );
		nBaseAlpha = fFade * 255.0f;
	}

	const float fadeDuration = 0.1f;
	if ( m_fading )
	{
		float fadeTimeRemaining = m_fadeStart + fadeDuration - gpGlobals->curtime;
		int alpha = nBaseAlpha * fadeTimeRemaining / fadeDuration;
		SetAlpha( alpha );
		if ( alpha <= 0 )
		{
			ShowRadialMenuPanel( false );
			SetRadialMenuEnabled( false );
			return;
		}
	}
	else
	{
		SetAlpha( nBaseAlpha );
	}

	int x, y, wide, tall;
	GetBounds( x, y, wide, tall );
	vgui::surface()->DrawSetColor( m_lineColor );

	int centerX = x + wide/2;
	int centerY = y + tall/2;
	float innerRadius = cl_rosette_line_inner_radius.GetFloat();
	float outerRadius = cl_rosette_line_outer_radius.GetFloat();
	innerRadius = YRES( innerRadius );
	outerRadius = YRES( outerRadius );

	// Draw horizontal and vertical lines
	if ( m_armedButtonDir != EAST && m_buttons[EAST]->IsVisible() && m_buttons[EAST]->IsEnabled() )
	{
		vgui::surface()->DrawLine( centerX + innerRadius, centerY, centerX + outerRadius, centerY );
	}
	
	if ( m_armedButtonDir != WEST && m_buttons[WEST]->IsVisible() && m_buttons[WEST]->IsEnabled() )
	{
		vgui::surface()->DrawLine( centerX - innerRadius, centerY, centerX - outerRadius, centerY );
	}
	
	if ( m_armedButtonDir != SOUTH && m_buttons[SOUTH]->IsVisible() && m_buttons[SOUTH]->IsEnabled() )
	{
		vgui::surface()->DrawLine( centerX, centerY + innerRadius, centerX, centerY + outerRadius );
	}
	
	if ( m_armedButtonDir != NORTH && m_buttons[NORTH]->IsVisible() && m_buttons[NORTH]->IsEnabled() )
	{
		vgui::surface()->DrawLine( centerX, centerY - innerRadius, centerX, centerY - outerRadius );
	}

	// Draw diagonal lines
	const float scale = 0.707f; // sqrt(2) / 2

	if ( m_armedButtonDir != SOUTH_EAST && m_buttons[SOUTH_EAST]->IsVisible() && m_buttons[SOUTH_EAST]->IsEnabled() )
	{
		vgui::surface()->DrawLine( centerX + innerRadius * scale, centerY + innerRadius * scale, centerX + outerRadius * scale, centerY + outerRadius * scale );
	}

	if ( m_armedButtonDir != NORTH_WEST && m_buttons[NORTH_WEST]->IsVisible() && m_buttons[NORTH_WEST]->IsEnabled() )
	{
		vgui::surface()->DrawLine( centerX - innerRadius * scale, centerY - innerRadius * scale, centerX - outerRadius * scale, centerY - outerRadius * scale );
	}

	if ( m_armedButtonDir != NORTH_EAST && m_buttons[NORTH_EAST]->IsVisible() && m_buttons[NORTH_EAST]->IsEnabled() )
	{
		vgui::surface()->DrawLine( centerX + innerRadius * scale, centerY - innerRadius * scale, centerX + outerRadius * scale, centerY - outerRadius * scale );
	}

	if ( m_armedButtonDir != SOUTH_WEST && m_buttons[SOUTH_WEST]->IsVisible() && m_buttons[SOUTH_WEST]->IsEnabled() )
	{
		vgui::surface()->DrawLine( centerX - innerRadius * scale, centerY + innerRadius * scale, centerX - outerRadius * scale, centerY + outerRadius * scale );
	}

	if ( RadialMenuDebug.GetBool() )
	{
		vgui::surface()->DrawSetColor( m_lineColor );
		vgui::surface()->DrawOutlinedRect( x + m_minButtonX, y + m_minButtonY, x + m_maxButtonX, y + m_maxButtonY );

		// draw the cursor position
		vgui::surface()->DrawLine( m_cursorX -10, m_cursorY, m_cursorX + 10, m_cursorY );
		vgui::surface()->DrawLine( m_cursorX , m_cursorY - 10 , m_cursorX, m_cursorY + 10 );
	}

	BaseClass::Paint();
}


void PlayDeactivateSound()
{
	C_BasePlayer *localPlayer = C_BasePlayer::GetLocalPlayer();
	// If we've got the player and they selected a different button
	if ( localPlayer )
	{
		localPlayer->EmitSound( "GameUI.UiCoopHudDeactivate" );
	}
}

//--------------------------------------------------------------------------------------------------------
void CRadialMenu::OnMousePressed( vgui::MouseCode code )
{
	if ( m_bEditing )
	{
		if ( code == MOUSE_LEFT )
		{
			StartDrag();
		}
	}

	if ( code == MOUSE_RIGHT )
	{
		StartFade();
		PlayDeactivateSound();
	}
	
	BaseClass::OnMousePressed( code );
}

void CRadialMenu::OnMouseReleased( vgui::MouseCode code )
{
	if ( m_bEditing )
	{
		if ( code == MOUSE_LEFT )
		{
			EndDrag();
		}
	}

	BaseClass::OnMouseReleased( code );
}

//--------------------------------------------------------------------------------------------------------
void CRadialMenu::OnCommand( const char *command )
{
	if ( RadialMenuDebug.GetBool() )
	{
		Msg( "%f: Clicked on button with command '%s'\n", gpGlobals->curtime, command );
	}

	if ( !Q_strcmp(command, "done") )
	{
		C_BasePlayer *localPlayer = C_BasePlayer::GetLocalPlayer();
		if ( localPlayer )
		{
			localPlayer->EmitSound("MouseMenu.abort");
		}
		StartFade();
	}
	else
	{
		// Undone... people hate clicking to select, so don't let them train themselves to do it
		//StartFade();
		//SendCommand( command );
	}

	BaseClass::OnCommand( command );
}


//--------------------------------------------------------------------------------------------------------
void CRadialMenu::SetArmedButtonDir( ButtonDir dir )
{
	if ( dir != NUM_BUTTON_DIRS )
	{
		CRadialButton *armedButton = m_buttons[dir];
		if ( m_buttons[dir]->GetPassthru() )
		{
			armedButton = m_buttons[dir]->GetPassthru();
			for ( int i=0; i<NUM_BUTTON_DIRS; ++i )
			{
				if ( m_buttons[i] == armedButton )
				{
					dir = (ButtonDir)i;
					break;
				}
			}
		}
	}

	if ( m_buttons[ dir ]->IsEnabled() )
	{
		C_BasePlayer *localPlayer = C_BasePlayer::GetLocalPlayer();
		// If we've got the player and they selected a different button
		if ( localPlayer && dir != m_armedButtonDir )
		{
			if( dir != CENTER )
			{
				localPlayer->EmitSound("GameUI.UiCoopHudFocus");
			}
			else
			{
				localPlayer->EmitSound("GameUI.UiCoopHudUnfocus");
			}
		}
	}

	m_armedButtonDir = dir;

	for ( int i=0; i<NUM_BUTTON_DIRS; ++i )
	{
		if ( !m_buttons[i] )
			continue;

		m_buttons[i]->SetFakeArmed( false );
		m_buttons[i]->SetArmed( false );
		if ( i != m_armedButtonDir )
		{
			m_buttons[i]->SetChosen( false );
		}
	}

	if ( m_armedButtonDir != NUM_BUTTON_DIRS )
	{
		if ( m_buttons[m_armedButtonDir] )
		{
#ifdef OSX
			if ( !m_buttons[m_armedButtonDir]->IsFakeArmed() )
			{
				m_buttons[m_armedButtonDir]->SetArmed( true );	
				OnCursorEnteredButton( m_cursorX, m_cursorY, m_buttons[m_armedButtonDir] );
			}
#endif
			m_buttons[m_armedButtonDir]->SetFakeArmed( true );
		}
	}

	if ( m_armedButtonDir != CENTER )
	{
		SetFadeInTime( gpGlobals->curtime - 1.0f );
	}
}


//--------------------------------------------------------------------------------------------------------
static const char *ButtonDirString( CRadialMenu::ButtonDir dir )
{
	switch ( dir )
	{
	case CRadialMenu::CENTER:
		return "CENTER";
	case CRadialMenu::NORTH:
		return "NORTH";
	case CRadialMenu::NORTH_EAST:
		return "NORTH_EAST";
	case CRadialMenu::EAST:
		return "EAST";
	case CRadialMenu::SOUTH_EAST:
		return "SOUTH_EAST";
	case CRadialMenu::SOUTH:
		return "SOUTH";
	case CRadialMenu::SOUTH_WEST:
		return "SOUTH_WEST";
	case CRadialMenu::WEST:
		return "WEST";
	case CRadialMenu::NORTH_WEST:
		return "NORTH_WEST";
	default:
		return "UNKNOWN";
	}
}


//--------------------------------------------------------------------------------------------------------
bool IsCurrentMenuTypeDisabled( C_Portal_Player *pPlayer, RadialMenuTypes_t menuType )
{
	// if the player isn't valid or is in the middle of taunting
	if ( !pPlayer || pPlayer->IsTaunting() )
	{
		return true;
	}

	// check the specific type of radial menu
	switch ( menuType )
	{
	case MENU_PING:
		if ( pPlayer->IsPingDisabled() )
		{
			return true;
		}
		break;
	case MENU_TAUNT:
		if ( pPlayer->IsTauntDisabled() )
		{
			return true;
		}
		break;
	case MENU_PLAYTEST:
		// don't allow playtest pings where normal pings aren't allowed
		if ( pPlayer->IsPingDisabled() 
#if defined( PORTAL2_PUZZLEMAKER )
			|| ( !sv_record_playtest.GetBool() && !engine->IsPlayingDemo() )
#endif // PORTAL2_PUZZLEMAKER
			)
		{
			return true;
		}
		break;
	}

	return false;
}


//--------------------------------------------------------------------------------------------------------
void CRadialMenu::OnCursorEnteredButton( int x, int y, CRadialButton *button )
{
	ButtonDir nNewDir = NUM_BUTTON_DIRS;

	for ( int i=0; i<NUM_BUTTON_DIRS; ++i )
	{
		if ( m_buttons[i] == button )
		{
			m_cursorX = x;
			m_cursorY = y;
			// If we've got the player and they selected a different button
			if ( (ButtonDir)i != m_armedButtonDir )
			{
				nNewDir = (ButtonDir)i;
				break;
			}

			if ( RadialMenuDebug.GetBool() )
			{
				Msg( "%f: rosette cursor entered %s at %d,%d\n", gpGlobals->curtime, ButtonDirString( m_armedButtonDir ), x, y );
				engine->Con_NPrintf( 20, "%d %d %s", x, y, ButtonDirString( m_armedButtonDir ) );
			}
		}
	}

	if ( nNewDir != NUM_BUTTON_DIRS && !input->ControllerModeActive() )
	{
		SetArmedButtonDir( nNewDir );
	}
}


//--------------------------------------------------------------------------------------------------------
void CRadialMenu::UpdateButtonBounds( void )
{
	// Save off the extents of our child buttons so we can clip the cursor to that later

	int wide, tall;
	GetSize( wide, tall );
	m_minButtonX = wide;
	m_minButtonY = tall;
	m_maxButtonX = 0;
	m_maxButtonY = 0;

	for ( int i=0; i<NUM_BUTTON_DIRS; ++i )
	{
		if ( !m_buttons[i] )
			continue;

		int hotspotMinX = 0;
		int hotspotMinY = 0;
		int hotspotMaxX = 0;
		int hotspotMaxY = 0;
		m_buttons[i]->GetHotspotBounds( &hotspotMinX, &hotspotMinY, &hotspotMaxX, &hotspotMaxY );

		int buttonX, buttonY;
		m_buttons[i]->GetPos( buttonX, buttonY );

		m_minButtonX = MIN( m_minButtonX, hotspotMinX + buttonX );
		m_minButtonY = MIN( m_minButtonY, hotspotMinY + buttonY );
		m_maxButtonX = MAX( m_maxButtonX, hotspotMaxX + buttonX );
		m_maxButtonY = MAX( m_maxButtonY, hotspotMaxY + buttonY );
	}

	// First frame, we won't have any hotspots set up, so we get inverted bounds from our initial setup.
	// Reverse these, so our button bounds covers our bounds.
	if ( m_minButtonX > m_maxButtonX )
	{
		V_swap( m_minButtonX, m_maxButtonX );
	}

	if ( m_minButtonY > m_maxButtonY )
	{
		V_swap( m_minButtonY, m_maxButtonY );
	}
}


//--------------------------------------------------------------------------------------------------------
void CRadialMenu::OnThink( void )
{
	int nSlot = vgui::ipanel()->GetMessageContextId( GetVPanel() );
	ACTIVE_SPLITSCREEN_PLAYER_GUARD( nSlot );

	if ( m_bQuickPingForceClose )
	{
		SetArmedButtonDir( CENTER );
	}

	C_Portal_Player *pPlayer = ToPortalPlayer( C_BasePlayer::GetLocalPlayer( nSlot ) );
	if ( IsCurrentMenuTypeDisabled( pPlayer, m_menuType ) )
	{
		CloseRadialMenuCommand( m_menuType );
	}

	if ( !IsMouseInputEnabled() )
		return;

	// See if our target entity has vanished
	if ( (m_menuType != MENU_TAUNT) && m_hTargetEntity == NULL && m_vPosition == vec3_invalid )
	{
		StartFade();
		return;
	}

	// See if we need to create our glow index
	if ( m_nEntityGlowIndex == -1 )
	{
		// dont glow players
		C_Portal_Player *pPlayer = dynamic_cast<C_Portal_Player *>(m_hTargetEntity.Get());
		if ( !pPlayer && m_menuType != MENU_TAUNT && ( m_hTargetEntity.Get() && m_hTargetEntity.Get()->GetRenderAlpha() > 0) )
		{
			C_BasePlayer *localPlayer = C_BasePlayer::GetLocalPlayer();
			if ( !localPlayer )
				return;

			m_nEntityGlowIndex = AddGlowToObject( m_hTargetEntity, localPlayer->GetTeamNumber() );
		}
	}
	else if ( m_menuType == MENU_TAUNT )
	{
		ClearGlowEntity();
	}

#if defined(OSX)
	int dx, dy;
	engine->GetMouseDelta( dx, dy );
	m_cursorX += dx;
	m_cursorY += dy;
#else
	vgui::surface()->SurfaceGetCursorPos( m_cursorX, m_cursorY );
	ScreenToLocal( m_cursorX, m_cursorY );
#endif

	if ( engine->IsRecordingDemo() )
	{
		int nMousePos[2] = { m_cursorX, m_cursorY };
		engine->RecordDemoCustomData( RadialMenuMouseCallback, &nMousePos, sizeof(int) * 2 );
	}
	else if ( engine->IsPlayingDemo() )
	{
		// use saved coords from the callback
		m_cursorX = m_demoCursorX;
		m_cursorY = m_demoCursorY;
	}
	
	int wide, tall;
	GetSize( wide, tall );

	int centerx = wide / 2;
	int centery = tall / 2;

	UpdateButtonBounds();

	if ( m_bFirstCentering )
	{
#ifndef OSX
		LocalToScreen( centerx, centery );
		vgui::surface()->SurfaceSetCursorPos( centerx, centery );
#endif
		m_cursorX = centerx;
		m_cursorY = centery;
		SetArmedButtonDir( CENTER );

		m_bFirstCentering = false;
	}
	else
	{

		float cursorDistX = ( m_cursorX - centerx );
		float cursorDistY = ( m_cursorY - centery );
		float buttonRadius = MAX( m_maxButtonX - centerx, m_maxButtonY - centery ) * 0.3f;

		if ( cursorDistX != 0.0f && cursorDistY != 0.0f )
		{
			float cursorDist = sqrt( Sqr(cursorDistX) + Sqr(cursorDistY) );

			// keeping mouse cursor within button radius
			if ( cursorDist > buttonRadius )
			{
				cursorDistX *= ( buttonRadius / cursorDist );
				cursorDistY *= ( buttonRadius / cursorDist );

				m_cursorX = centerx + cursorDistX;
				m_cursorY = centery + cursorDistY;
			}

#ifndef OSX
			LocalToScreen( m_cursorX, m_cursorY );
			if ( RadialMenuDebug.GetBool() )
			{
				Msg( "%f: rosette warping cursor to %d %d\n", gpGlobals->curtime, m_cursorX, m_cursorY );
			}
			
			vgui::surface()->SurfaceSetCursorPos( m_cursorX, m_cursorY );
#endif
		}

		float fJoyForward, fJoySide, fJoyPitch, fJoyYaw = 0.0f;

		if ( input->ControllerModeActive() )
		{
			input->Joystick_Querry( fJoyForward, fJoySide, fJoyPitch, fJoyYaw );

			// Replace if the other stick was pushed further
			// We need to use both sticks because they might have southpaw or legacy set
			if ( fabsf( fJoyForward ) > fabsf( fJoyPitch ) )
			{
				fJoyPitch = fJoyForward;
			}
			else
			{
				// Reflip it if the y was inverted because this shouldn't be inverted
				static SplitScreenConVarRef s_joy_inverty( "joy_inverty" );
				if ( s_joy_inverty.IsValid() && s_joy_inverty.GetBool( nSlot ) )
				{
					fJoyPitch *= -1.0f;
				}
			}

			// Replace if the other stick was pushed further
			// We need to use both sticks because they might have southpaw or legacy set
			if ( fabsf( fJoySide ) > fabsf( fJoyYaw ) )
			{
				fJoyYaw = fJoySide;
			}

		}
		else
		{
			fJoyPitch = cursorDistY / buttonRadius;
			fJoyYaw = cursorDistX / buttonRadius;
		}

		ButtonDir dir = CENTER;
		// Right stick can select
		if ( fabsf( fJoyPitch ) > 0.3f || fabsf( fJoyYaw ) > 0.3f )
		{
			// Right stick is past the dead zone
			float fStickAngle = RAD2DEG( atan2( fJoyYaw, fJoyPitch ) );

			if ( fStickAngle > 157.5f )
			{
				dir = NORTH;
			}
			else if ( fStickAngle > 112.5f )
			{
				dir = NORTH_EAST;
			}
			else if ( fStickAngle > 67.5f )
			{
				dir = EAST;
			}
			else if ( fStickAngle > 22.5f )
			{
				dir = SOUTH_EAST;
			}
			else if ( fStickAngle > -22.5f )
			{
				dir = SOUTH;
			}
			else if ( fStickAngle > -67.5f )
			{
				dir = SOUTH_WEST;
			}
			else if ( fStickAngle > -112.5f )
			{
				dir = WEST;
			}
			else if ( fStickAngle > -157.5f )
			{
				dir = NORTH_WEST;
			}
			else
			{
				dir = NORTH;
			}
		}

		CRadialButton *pButton = m_buttons[dir];
		if ( pButton && pButton->IsVisible() && pButton->IsEnabled() && !pButton->IsArmed() && m_armedButtonDir != dir )
		{
			// Only allow going back to center if a tiny amount of time has passed
			if ( !input->ControllerModeActive() || m_fSelectionLockInTime == 0.0f || gpGlobals->curtime < m_fSelectionLockInTime + cl_rosette_gamepad_lockin_time.GetFloat() || dir != CENTER )
			{
				m_fSelectionLockInTime = gpGlobals->curtime;
				pButton->SetMaxScale( dir == CENTER ? 1.0f : 0.75f );
				SetArmedButtonDir( dir );
			}
		}

		if ( m_armedButtonDir != CENTER && m_armedButtonDir != NUM_BUTTON_DIRS )
		{
			float fInterp = RemapValClamped( ( gpGlobals->curtime - m_fSelectionLockInTime ), 0.0f, cl_rosette_gamepad_expand_time.GetFloat(), 0.0f, 1.0f );

			CRadialButton *pSelectedButton = m_buttons[ m_armedButtonDir ];
			pSelectedButton->SetMaxScale( 0.75f + 0.25f * fInterp );

			if ( input->ControllerModeActive() && fInterp >= 1.0f )
			{
				// take action as soon as the player select something on the menu
				CloseRadialMenuCommand( m_menuType );
			}
		}
	}
}


void CRadialMenu::OnTick( void )
{
	BaseClass::OnTick();

	for ( int nPlayer = 0; nPlayer < MAX_SPLITSCREEN_PLAYERS; ++nPlayer )
	{
		ACTIVE_SPLITSCREEN_PLAYER_GUARD( nPlayer );
		CUtlVector< SignifierInfo_t > *pSignifierQueue = GetSignifierQueue();
		for ( int i = pSignifierQueue->Count() - 1; i >= 0; --i )
		{
			if ( (*pSignifierQueue)[ i ].fDisplayTime <= gpGlobals->curtime )
			{
				AddLocator( (*pSignifierQueue)[ i ].hTarget, (*pSignifierQueue)[ i ].vPos, (*pSignifierQueue)[ i ].vNormal, (*pSignifierQueue)[ i ].nPlayerIndex, (*pSignifierQueue)[ i ].szCaption, 0.0f );
				pSignifierQueue->Remove( i );
			}
		}
	}
}


static const char *s_pszRadialMenuIgnoreActions[] =
{
	"forward",
	"back",
	"moveleft",
	"moveright",
	"jump",
	"duck",
	"attack",
	"attack2"
};

int	CRadialMenu::KeyInput( int down, ButtonCode_t keynum, const char *pszCurrentBinding )
{
	static bool bViewLocked = false;
	if ( !IsVisible() )
		return 1;

	if ( !down )
		return 1;

	ASSERT_LOCAL_PLAYER_RESOLVABLE();
	int nSlot = GET_ACTIVE_SPLITSCREEN_SLOT();

	int numIgnore = ARRAYSIZE( s_pszRadialMenuIgnoreActions );
	for ( int i=0; i<numIgnore; ++i )
	{
		int count = 0;
		ButtonCode_t key;
		do 
		{
			key = (ButtonCode_t)engine->Key_CodeForBinding( s_pszRadialMenuIgnoreActions[i], nSlot, count, -1 );
			if ( IsJoystickCode( key ) )
			{
				key = GetBaseButtonCode( key );
			}

			if ( keynum == key )
			{
				return 0;
			}
			++count;
		} while ( key != BUTTON_CODE_INVALID );
	}

	return 1;
}


//--------------------------------------------------------------------------------------------------------
void CRadialMenu::SendCommand( const char *commandStr )
{
	if ( V_strcmp( commandStr, "done" ) == 0 )
	{
		return;
	}

	// Setup the basic command
	char szClientCmd[512];

	// Tack on our target entity index
	int nEntityIndex = ( m_hTargetEntity ) ? m_hTargetEntity->entindex() : -1;

	bool bDelayed = false;
	bool bIsTaunt = false;

	if ( StringHasPrefix( commandStr, "taunt" ) )
	{
		bIsTaunt = true;

		V_strcpy( szClientCmd, commandStr );

		const char *pchTaunt = commandStr + V_strlen( "taunt" );
		if ( pchTaunt[ 0 ] == ' ' && pchTaunt[ 1 ] != '\0' )
		{
			pchTaunt++;

			GetClientMenuManagerTaunt().IsTauntTeam( pchTaunt );
			GetClientMenuManagerTaunt().SetTauntUsed( pchTaunt );
		}
	}
	else
	{
		if ( V_strcmp( commandStr, "countdown" ) == 0 )
		{
			bDelayed = true;

			if ( GetSignifierQueue()->Count() )
			{
				// Don't start a new count till the old one finished!
				return;
			}
		}

		// Build a super string (ick)
		V_snprintf( szClientCmd, sizeof( szClientCmd ), "signify %s %d %.2f %.2f %.2f %.2f %.2f %.2f %.2f", 
			commandStr,
			nEntityIndex,
			( bDelayed ? 0.3f : 0.0f ),
			m_vPosition.x, m_vPosition.y, m_vPosition.z,
			m_vNormal.x, m_vNormal.y, m_vNormal.z );
	}
	
	// Send it on its way
	engine->ClientCmd( szClientCmd );

	if ( !bDelayed && !bIsTaunt )
	{
		// Now send to ourself first
		C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
		if ( pPlayer )
		{
			AddLocator( m_hTargetEntity, m_vPosition, m_vNormal, pPlayer->entindex(), commandStr, 0.0f );
		}
	}
}

//--------------------------------------------------------------------------------------------------------
void CRadialMenu::ChooseArmedButton()
{
	StartFade();

	CRadialButton *button = NULL;
	for ( int i=NUM_BUTTON_DIRS-1; i>=0; --i )
	{
		if ( !m_buttons[i] )
			continue;

		if ( m_buttons[i]->IsVisible() && m_buttons[i]->IsEnabled() && m_buttons[i]->IsArmed() && !m_buttons[i]->GetPassthru() )
		{
			if ( RadialMenuDebug.GetBool() )
			{
				Msg( "%f: Choosing armed button %s\n", gpGlobals->curtime, ButtonDirString( (ButtonDir)i ) );
			}
			button = m_buttons[i];
			break;
		}
	}

	if ( !button && m_armedButtonDir != NUM_BUTTON_DIRS )
	{
		if ( m_buttons[m_armedButtonDir] && m_buttons[m_armedButtonDir]->IsVisible() && m_buttons[m_armedButtonDir]->IsEnabled() )
		{
			if ( RadialMenuDebug.GetBool() )
			{
				Msg( "%f: Choosing saved armed button %s\n", gpGlobals->curtime, ButtonDirString( m_armedButtonDir ) );
			}
			button = m_buttons[m_armedButtonDir];
		}
	}

	if ( button )
	{
		KeyValues *command = button->GetCommand();
		if ( command )
		{
			const char *commandStr = command->GetString( "command", NULL );
			if ( commandStr )
			{
				button->SetChosen( true );
				SendCommand( commandStr );

				if ( button->GetGLaDOSResponse() > 0 )
				{
					C_BasePlayer *localPlayer = C_BasePlayer::GetLocalPlayer();
					if ( localPlayer )
					{
						char szCmd[ 32 ];
						V_snprintf( szCmd, sizeof(szCmd), "CoopPingTool(%i,%i)", ( localPlayer->GetTeamNumber() == TEAM_RED ? 1 : 2 ), button->GetGLaDOSResponse() );
						engine->ClientCmd( szCmd );
					}
				}
			}
		}

		for ( int i=0; i<NUM_BUTTON_DIRS; ++i )
		{
			if ( !m_buttons[i] )
				continue;

			if ( m_buttons[i] == button )
			{
				if( m_menuType == MENU_TAUNT && i == CENTER )
				{
					PlayDeactivateSound();
				}
				continue;
			}

			m_buttons[i]->SetFakeArmed( false );
			m_buttons[i]->SetChosen( false );
		}
	}
}


//--------------------------------------------------------------------------------------------------------
void CRadialMenu::StartFade( void )
{
	m_fading = true;
	m_fadeStart = gpGlobals->curtime;
	SetMouseInputEnabled( false );

	ClearGlowEntity();	
}


//--------------------------------------------------------------------------------------------------------
void CRadialMenu::SetData( KeyValues *data )
{
	if ( !data )
		return;

	if ( RadialMenuDebug.GetBool() )
	{
		m_resource->deleteThis();
		m_resource = new KeyValues( "RadialMenu" );
		m_resource->LoadFromFile( filesystem, "resource/UI/RadialMenu.res" );
	}

	if ( m_menuData != data )
	{
		if ( m_menuData )
		{
			m_menuData->deleteThis();
		}

		m_menuData = data->MakeCopy();
	}

	bool bDisabledButtons[ NUM_BUTTON_DIRS ];
	memset( bDisabledButtons, 0, sizeof( bDisabledButtons ) );

	for ( int i=0; i<NUM_BUTTON_DIRS; ++i )
	{
		if ( !m_buttons[i] )
			continue;

		ButtonDir dir = (ButtonDir)i;
		const char *buttonName = ButtonNameFromDir( dir );
		KeyValues *buttonData = data->FindKey( buttonName, false );
		if ( !buttonData )
		{
			m_buttons[i]->SetVisible( false );
			continue;
		}

		KeyValues *resourceControl = m_resource->FindKey( buttonName, false );
		if ( resourceControl )
		{
			m_buttons[i]->UpdateHotspots( resourceControl );
			m_buttons[i]->InvalidateLayout();
		}

		m_buttons[i]->SetVisible( true );
		m_buttons[i]->SetChosen( false );

		const char *text = buttonData->GetString( "text" );
		m_buttons[i]->SetText( text );

		const char *command = buttonData->GetString( "command" );
		m_buttons[i]->SetCommand( command );

		const char *image = buttonData->GetString( "icon" );
		if ( image && image[0] != '\0' )
		{
			const char *image2 = buttonData->GetString( "icon2" );
			if ( image2 && image2[0] != '\0' )
			{
				// Lets decide which image to use based on team
				C_BasePlayer *localPlayer = C_BasePlayer::GetLocalPlayer();
				if ( localPlayer && localPlayer->GetTeamNumber() == TEAM_RED )
				{
					m_buttons[i]->SetImage( image2 );
				}
				else
				{
					m_buttons[i]->SetImage( image );
				}
			}
			else
			{
				// Only one image possible
				m_buttons[i]->SetImage( image );
			}
		}

		if ( !command || !*command )
		{
			bDisabledButtons[ i ] = true;
			m_buttons[i]->SetEnabled( false );
			m_buttons[i]->SetPulse( false );
		}
		else
		{
			m_buttons[i]->SetEnabled( true );
			m_buttons[i]->SetPulse( false );
			//m_buttons[i]->SetPulse( buttonData->GetBool( "new" ) );
		}

		m_buttons[i]->SetGLaDOSResponse( buttonData->GetInt( "glados", 0 ) );

		m_buttons[i]->ShowSubmenuIndicator( Q_strncasecmp( "radialmenu ", command, Q_strlen( "radialmenu " ) ) == 0 );

		const char *owner = buttonData->GetString( "owner", NULL );
		if ( owner )
		{
			ButtonDir dir = DirFromButtonName( owner );
			m_buttons[i]->SetPassthru( m_buttons[dir] );
		}
		else
		{
			m_buttons[i]->SetPassthru( NULL );
		}

		m_buttons[i]->SetArmed( false );
	}
}

//--------------------------------------------------------------------------------------------------------
void CRadialMenu::ClearGlowEntity( void )
{
	// Stop glowing if we're done
	if ( m_nEntityGlowIndex != -1 )
	{
		g_GlowObjectManager.UnregisterGlowObject( m_nEntityGlowIndex );
		m_nEntityGlowIndex = -1;
	}
}

//--------------------------------------------------------------------------------------------------------
void CRadialMenu::OnRadialMenuOpen( void )
{
	if ( m_nArrowTexture == -1 )
	{
		m_nArrowTexture = vgui::surface()->CreateNewTextureID();
		vgui::surface()->DrawSetTextureFile( m_nArrowTexture, RADIAL_MENU_POINTER_TEXTURE, true, false );
	}

	m_fading = false;
	m_fadeStart = 0.0f;

	vgui::Button *firstButton = NULL;
	for ( int i=0; i<NUM_BUTTON_DIRS; ++i )
	{
		if ( !m_buttons[i] || !m_buttons[i]->IsVisible() || !m_buttons[i]->IsEnabled() )
			continue;

		if ( firstButton )
		{
			// already found another valid button.  since we have at least 2, we can show the menu.
			SetMouseInputEnabled( true );
			return;
		}

		firstButton = m_buttons[i];
	}
}


//--------------------------------------------------------------------------------------------------------
void FlushClientMenus( void )
{
	TheClientMenuManager.Flush();

	TheClientMenuManagerPlaytest.Flush();

	for ( int i = 0; i < MAX_SPLITSCREEN_PLAYERS; ++i )
	{
		GetClientMenuManagerTaunt( i ).Flush();
	}
}


//--------------------------------------------------------------------------------------------------------
void OpenRadialMenu( const char *lpszTargetClassification, EHANDLE hTargetEntity, const Vector &vPosition, const Vector &vNormal, RadialMenuTypes_t menuType )
{
	// FIXME: Need the equivalent here...
	// if ( GetTerrorClientMode() && GetTerrorClientMode()->IsInTransition() )
	//	return;

	ASSERT_LOCAL_PLAYER_RESOLVABLE();
	int nSlot = GET_ACTIVE_SPLITSCREEN_SLOT();

	C_Portal_Player *localPlayer = ToPortalPlayer( C_BasePlayer::GetLocalPlayer( nSlot ) );
	if ( IsCurrentMenuTypeDisabled( localPlayer, menuType ) )
	{
		return;
	}

	CRadialMenu *pRadialMenu = GET_HUDELEMENT( CRadialMenu );
	if ( !pRadialMenu )
		return;

	pRadialMenu->SetQuickPingForceClose( false );

	const char *pchTarget = ( ( lpszTargetClassification && lpszTargetClassification[ 0 ] != '\0' ) ? ( lpszTargetClassification ) : ( "Default" ) );

	if ( FStrEq( s_radialMenuName[ nSlot ], pchTarget ) )
	{
		bool wasOpen = pRadialMenu->IsVisible() && !pRadialMenu->IsFading();
		if ( wasOpen )
		{
			pRadialMenu->SetRadialType( menuType );

			return;
		}
	}

	if ( RadialMenuDebug.GetBool() )
	{
		FlushClientMenus(); // for now, reload every time
	}

	// Msg("Hit: %s\n", pchTarget );

	ClientMenuManager *pMM;
	if ( menuType == MENU_TAUNT )
	{
		pMM = &GetClientMenuManagerTaunt();
	}
	else if ( menuType == MENU_PING )
	{
		pMM = &TheClientMenuManager;
	}
	else
	{
		pMM = &TheClientMenuManagerPlaytest;
	}

	KeyValues *menuKey = pMM->FindMenu( pchTarget );
	if ( menuKey == NULL )
	{
		menuKey = pMM->FindMenu( "Default" );
		if ( menuKey == NULL )
		{
			DevMsg( "No client menu currently matches %s\n", pchTarget );

			ShowRadialMenuPanel( false );
			pRadialMenu->SetRadialMenuEnabled( false );
			return;
		}
	}

	V_snprintf( s_radialMenuName[ nSlot ], sizeof( s_radialMenuName[ nSlot ] ), pchTarget );
	pRadialMenu->SetData( menuKey );

	pRadialMenu->SetRadialType( menuType );

	if ( menuType == MENU_TAUNT || input->ControllerModeActive() )
	{
		pRadialMenu->SetFadeInTime( gpGlobals->curtime - 1.0f );
	}
	else
	{
		pRadialMenu->SetFadeInTime( gpGlobals->curtime + 0.5f );
	}

	pRadialMenu->ClearLockInTime();

	pRadialMenu->m_bFirstCentering = true;

	// Send up the specific data we need passed in
	pRadialMenu->SetTargetEntity( hTargetEntity );
	pRadialMenu->SetTraceData( vPosition, vNormal );

	if ( localPlayer )
	{
		localPlayer->EmitSound( "GameUI.UiCoopHudActivate" );
	}

	ShowRadialMenuPanel( true );
	pRadialMenu->SetRadialMenuEnabled( true );
	pRadialMenu->OnRadialMenuOpen();
}


//-----------------------------------------------------------------------------
// Purpose: Find and categorize our intended command target and let the client know
//-----------------------------------------------------------------------------
bool LaunchRadialMenu( int nPlayerSlot, RadialMenuTypes_t menuType )
{
	char szTarget[ MAX_PATH ] = { 0 };
	CBaseEntity *pTargetEntity = NULL;

	// Get our local target
	C_Portal_Player *pPlayer = ToPortalPlayer( C_BasePlayer::GetLocalPlayer( nPlayerSlot ) );
	if ( IsCurrentMenuTypeDisabled( pPlayer, menuType ) )
	{
		return false;
	}

	int nTeamSlot = ( pPlayer->GetTeamNumber() == TEAM_BLUE ? 0 : 1 );

	float *pfLastPingTime = &( CRadialMenu::m_fLastPingTime[ GET_ACTIVE_SPLITSCREEN_SLOT() ][ nTeamSlot ] );
	int *pfNumPings = &( CRadialMenu::m_nNumPings[ GET_ACTIVE_SPLITSCREEN_SLOT() ][ nTeamSlot ] );

	if ( menuType == MENU_TAUNT )
	{
		if ( pPlayer->PredictedAirTimeEnd() > 0.85f )
		{
			engine->ClientCmd( "taunt" );

			IGameEvent *event = gameeventmanager->CreateEvent( "player_gesture" );
			if ( event )
			{
				event->SetInt( "userid", pPlayer->GetUserID() );
				event->SetBool( "air", true );
				gameeventmanager->FireEventClientSide( event );
			}
			return false;
		}
	}
	else
	{
		float fNextPingTime = *pfLastPingTime + PING_DELAY_BASE + PING_DELAY_INCREMENT * *pfNumPings;
		float fPastNextPingTime = gpGlobals->curtime - fNextPingTime;

		if ( gpGlobals->curtime > *pfLastPingTime && fPastNextPingTime < 0.0f )
		{
			// They've been spamming pings, don't let them place another for now
			return false;
		}
	}

	// Clear this out for safety
	Vector vPosition = vec3_invalid;
	Vector vNormal = vec3_invalid;

	if ( menuType != MENU_TAUNT )
	{
		// Find what's under the cursor
		Vector vForward;
		pPlayer->EyeVectors( &vForward );
		Vector vEyeTrace = pPlayer->EyePosition() + ( vForward * MAX_TRACE_LENGTH );

		Ray_t ray;
		ray.Init( pPlayer->EyePosition(), vEyeTrace );

		float flTheNumberOne = 1.0f;
		C_Portal_Base2D *pHitPortal = UTIL_Portal_FirstAlongRay( ray, flTheNumberOne );
		C_Prop_Portal *pPropPoral = dynamic_cast<C_Prop_Portal *>(pHitPortal);

		trace_t tr;
		// Do a trace that respects portals (allows for portal-linked doors)
		CTraceFilterNoPlayers filter1;
		CTraceFilterSkipTwoEntities filter2( GetPlayerHeldEntity( pPlayer ), pPlayer->GetAttachedObject() );
		CTraceFilterChain filter( &filter1, &filter2 );
		UTIL_Portal_TraceRay( ray, (MASK_OPAQUE_AND_NPCS|CONTENTS_SLIME), &filter, &tr );

		pPlayer->CreatePingPointer( tr.endpos );

		// Did we hit a portal?
		if ( pHitPortal && pHitPortal->IsActivedAndLinked() && pPropPoral )
		{
			V_snprintf( szTarget, sizeof(szTarget), "Portal.%s", pHitPortal->m_bIsPortal2 ? "Orange" : "Blue" );
			pTargetEntity = pHitPortal;
		}
		else
		{
			// See if we passed through a tractor bream
			Ray_t ray;

			if ( !( ( tr.DidHitWorld() || ( tr.m_pEnt && tr.m_pEnt->IsBrushModel() ) ) && !( tr.contents & CONTENTS_SLIME ) && !IsNoPortalMaterial( tr ) ) )
			{
				// It's not a portal surface, so maybe they wanted to hit the bridge or tbeam
				for ( int i = 0; i < ITriggerTractorBeamAutoList::AutoList().Count(); ++i )
				{
					C_Trigger_TractorBeam *pTractorBeam = static_cast< C_Trigger_TractorBeam* >( ITriggerTractorBeamAutoList::AutoList()[ i ] );

					ray.Init( tr.startpos, tr.endpos );

					trace_t trTemp;
					enginetrace->ClipRayToEntity( ray, MASK_SHOT, pTractorBeam, &trTemp );

					if ( !trTemp.startsolid && ( trTemp.fraction < 1.0f || trTemp.m_pEnt == pTractorBeam ) )
					{
						tr = trTemp;
						tr.m_pEnt = ClientEntityList().GetBaseEntity( 0 );
						tr.surface.flags |= SURF_NOPORTAL;

						// Fix up the surface normal and ping position
						Vector vPointOnPath;
						CalcClosestPointOnLineSegment( tr.endpos, pTractorBeam->GetStartPoint(), pTractorBeam->GetEndPoint(), vPointOnPath, NULL );

						tr.plane.normal = tr.endpos - vPointOnPath;
						VectorNormalize( tr.plane.normal );

						tr.endpos = vPointOnPath + tr.plane.normal * pTractorBeam->GetBeamRadius();
					}
				}

				// See if we passed through a light bridge
				for ( int i = 0; i < IProjectedWallEntityAutoList::AutoList().Count(); ++i )
				{
					C_ProjectedWallEntity *pLightBridge = static_cast< C_ProjectedWallEntity* >( IProjectedWallEntityAutoList::AutoList()[ i ] );
					Vector vBridgeUp = pLightBridge->Up();
					if ( vBridgeUp.z > -0.4f && vBridgeUp.z < 0.4f )
					{
						// Don't hit wall bridges
						continue;
					}

					ray.Init( tr.startpos, tr.endpos );

					trace_t trTemp;
					enginetrace->ClipRayToEntity( ray, MASK_SHOT, pLightBridge, &trTemp );

					if ( trTemp.fraction < 1.0f || trTemp.m_pEnt == pLightBridge )
					{
						tr = trTemp;
						tr.m_pEnt = ClientEntityList().GetBaseEntity( 0 );
						tr.surface.flags |= SURF_NOPORTAL;

						if ( tr.plane.normal.z < -0.9f )
						{
							// Point down at the bridge from above so we can tell players to stand on it even when pointing from below
							tr.plane.normal.z = -tr.plane.normal.z;
							tr.endpos.z += 2.0f;
						}
					}
				}
			}

			// If it's an entity, just return that
			if ( tr.m_pEnt && tr.DidHitNonWorldEntity() && !tr.m_pEnt->IsBrushModel() )
			{
				// Fill out the details
				const char *lpszSignifier = tr.m_pEnt->GetSignifierName();

				Assert( lpszSignifier != NULL );
				V_snprintf( szTarget, sizeof(szTarget), "Entity.%s", lpszSignifier );

				// Initially, use this as the target entity
				pTargetEntity = tr.m_pEnt;

				if ( pTargetEntity && V_strstr( szTarget, "button" ) )
				{
					Vector vecBoundsMax, vecBoundsMin;
					pTargetEntity->GetRenderBounds( vecBoundsMin, vecBoundsMax );
					vPosition = pTargetEntity->WorldSpaceCenter();
					vPosition.z += (vecBoundsMax.z - 6);
					vNormal = Vector( 0, 0, 1 );
				}

				// Access the entity interface methods to get extra data
				ISignifierTarget *pSignifier = dynamic_cast<ISignifierTarget *>(tr.m_pEnt);
				if ( pSignifier != NULL )
				{
					// See if we're overriding our hit position
					if ( pSignifier->OverrideSignifierPosition() )
					{
						pTargetEntity = NULL;
						pSignifier->GetSignifierPosition( tr.endpos, vPosition, vNormal );
					}
				}
			}
			else if ( tr.DidHitWorld() || (tr.m_pEnt && tr.m_pEnt->IsBrushModel()) )
			{
				// We're going to use this position explicitly
				vPosition = tr.endpos;
				vNormal = tr.plane.normal;

				if ( tr.contents & CONTENTS_SLIME )
				{
					V_strncpy( szTarget, "World.Slime", sizeof(szTarget) );
				}
				else
				{
					if ( IsNoPortalMaterial( tr ) )
					{
						V_strncpy( szTarget, "World.NoPortal_", sizeof(szTarget) );
					}
					else
					{
						V_strncpy( szTarget, "World.", sizeof(szTarget) );
					}

					// If it's the world, then classify it
					if ( tr.plane.normal[2] > 0.75f )
					{
						V_strncat( szTarget, "Floor", sizeof(szTarget) );
					}
					else if ( tr.plane.normal[2] < -0.75f )
					{
						V_strncat( szTarget, "Ceiling", sizeof(szTarget) );
					}
					else
					{
						V_strncat( szTarget, "Wall", sizeof(szTarget) );
					}
				}
			}
			else
			{
				// Unknown entity type!
				Assert( 0 );
				return false;
			}
		}
	}

	// Launch the real menu
	OpenRadialMenu( szTarget, pTargetEntity, vPosition, vNormal, menuType );

	return true;
}

//--------------------------------------------------------------------------------------------------------
bool IsRadialMenuOpen( void )
{
	// Determine whether this window is visible or not
	CRadialMenu *pRadialMenu = GET_HUDELEMENT( CRadialMenu );
	if ( pRadialMenu )
	{
		bool isOpen = pRadialMenu->IsVisible() && !pRadialMenu->IsFading();
		return isOpen;
	}

	return false;
}

//--------------------------------------------------------------------------------------------------------
bool OpenRadialMenuCommand( RadialMenuTypes_t menuType )
{
	if ( !g_pGameRules )
		return false;

	if ( menuType != MENU_PLAYTEST && !g_pGameRules->IsMultiplayer() )
		return false;

	ASSERT_LOCAL_PLAYER_RESOLVABLE();
	int nSlot = GET_ACTIVE_SPLITSCREEN_SLOT();

	if ( s_mouseMenuKeyHeld[ nSlot ] )
		return true;

	bool bSuccess = LaunchRadialMenu( nSlot, menuType );

	if ( bSuccess )
	{
		s_mouseMenuKeyHeld[ nSlot ] = true;
	}

	return bSuccess;
}

void openradialmenu( const CCommand &args )
{
	OpenRadialMenuCommand( MENU_PING );
}
static ConCommand mouse_menu_open( "+mouse_menu", openradialmenu, "Opens a menu while held" );

void openradialmenutaunt( const CCommand &args )
{
	OpenRadialMenuCommand( MENU_TAUNT );
}
static ConCommand mouse_menu_taunt_open( "+mouse_menu_taunt", openradialmenutaunt, "Opens a menu while held" );

void openradialmenuplaytest( const CCommand &args )
{
	OpenRadialMenuCommand( MENU_PLAYTEST );
}
static ConCommand mouse_menu_playtest_open( "+mouse_menu_playtest", openradialmenuplaytest, "Opens a menu while held" );

//--------------------------------------------------------------------------------------------------------
void CloseRadialMenuCommand( RadialMenuTypes_t menuType, bool bForceClose /*= false*/ )
{
	if ( menuType != MENU_PLAYTEST && ( !g_pGameRules || !g_pGameRules->IsMultiplayer() ) )
		return;

	ASSERT_LOCAL_PLAYER_RESOLVABLE();
	int nSlot = GET_ACTIVE_SPLITSCREEN_SLOT();

	CRadialMenu *pRadialMenu = GET_HUDELEMENT( CRadialMenu );
	if ( !pRadialMenu )
		return;

	// Get our local target
	C_Portal_Player *pPlayer = ToPortalPlayer( C_BasePlayer::GetLocalPlayer( nSlot ) );
	if ( pPlayer )
		pPlayer->DestroyPingPointer();

	s_mouseMenuKeyHeld[ nSlot ] = false;

	if ( !cl_fastradial.GetBool() )
	{
		return;
	}

	bool wasOpen = pRadialMenu->IsVisible() && !pRadialMenu->IsFading();

	if ( wasOpen || bForceClose )
	{
		pRadialMenu->ChooseArmedButton();

		int wide, tall;
		pRadialMenu->GetSize( wide, tall );
		wide /= 2;
		tall /= 2;
		pRadialMenu->LocalToScreen( wide, tall );
		vgui::surface()->SurfaceSetCursorPos( wide, tall );

		if ( bForceClose )
		{
			ShowRadialMenuPanel( false );
			pRadialMenu->SetRadialMenuEnabled( false );
		}

		if ( !bForceClose )
		{
			input->Joystick_ForceRecentering( 0 );
			input->Joystick_ForceRecentering( 1 );
		}
	}
	else if ( !pRadialMenu->IsVisible() )
	{
		ShowRadialMenuPanel( false );
	}

	s_radialMenuName[ nSlot ][0] = 0;
}

void closeradialmenu( const CCommand &args )
{
	CloseRadialMenuCommand( MENU_PING );
}
static ConCommand mouse_menu_close( "-mouse_menu", closeradialmenu, "Executes the highlighted button on the radial menu (if cl_fastradial is 1)" );

void closeradialmenutaunt( const CCommand &args )
{
	CloseRadialMenuCommand( MENU_TAUNT );
}
static ConCommand mouse_menu_taunt_close( "-mouse_menu_taunt", closeradialmenutaunt, "Executes the highlighted button on the radial menu (if cl_fastradial is 1)" );

void closeradialmenuplaytest( const CCommand &args )
{
	CloseRadialMenuCommand( MENU_PLAYTEST );
}
static ConCommand mouse_menu_playtest_close( "-mouse_menu_playtest", closeradialmenuplaytest, "Executes the highlighted button on the radial menu (if cl_fastradial is 1)" );

extern bool UTIL_EntityBoundsToSizes( C_BaseEntity *pTarget, int *pMinX, int *pMinY, int *pMaxX, int *pMaxY );
extern bool UTIL_WorldSpaceToScreensSpaceBounds( const Vector &vecCenter, const Vector &mins, const Vector &maxs, Vector2D *pMins, Vector2D *pMaxs );

void cc_quickping( const CCommand &args )
{
	if ( !g_pGameRules || !g_pGameRules->IsMultiplayer() )
		return;

	if ( !OpenRadialMenuCommand( MENU_PING ) )
	{
		return;
	}

	CRadialMenu *pRadialMenu = GET_HUDELEMENT( CRadialMenu );
	if ( pRadialMenu )
	{
		pRadialMenu->SetArmedButtonDir( CRadialMenu::CENTER );
		pRadialMenu->SetQuickPingForceClose( true );
	}
	else
	{
		CloseRadialMenuCommand( MENU_PING, true );
	}
}
static ConCommand quick_ping("+quick_ping", cc_quickping, "Ping the center option from the ping menu.");

void closequickping( const CCommand &args )
{
}
static ConCommand quick_ping_close( "-quick_ping", closequickping, "Quick ping is unpressed... nothing to do here." );

//--------------------------------------------------------------------------------------------------------
class CSignifierSystem : public CAutoGameSystemPerFrame
{
	struct SignifierData_t
	{
		SignifierData_t( int index, int glowIndex, int playerIndex, int splitscreenID, EHANDLE hEntity, const Vector &vOrigin, float lifetime ) : 
			m_nLocatorIndex( index ), 
			m_nGlowIndex( glowIndex ),
			m_nPlayerIndex( playerIndex ),
			m_nSplitscreenID( splitscreenID ),
			m_hTargetEntity( hEntity), 
			m_vOrigin( vOrigin ),
			m_flLifetime( lifetime )
		{
		}

		int		m_nLocatorIndex;
		int		m_nGlowIndex;
		EHANDLE	m_hTargetEntity;
		Vector	m_vOrigin;
		float	m_flLifetime;
		int		m_nPlayerIndex;
		int		m_nSplitscreenID;
	};

public:
	CSignifierSystem() : CAutoGameSystemPerFrame( "CSignifierSystem" ) {}
	
	int GetObjectScreenHeight( C_BaseEntity *pObject )
	{
		if ( pObject == NULL )
			return 0;

		int minY = 0;
		int maxY = 0;
		UTIL_EntityBoundsToSizes( pObject, NULL, &minY, NULL, &maxY );

		return abs( maxY - minY );
	}

	// Pre-render
	virtual void PreRender( void ) 
	{
		C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
		bool bDead = pPlayer && !pPlayer->IsAlive();	

		// Work through all our indicators and keep them up to date
		FOR_EACH_VEC_BACK( m_Signifiers, itr )
		{
			CLocatorTarget *pLocator = Locator_GetTargetFromHandle( m_Signifiers[itr].m_nLocatorIndex );
			if ( pLocator )
			{
				const int HEIGHT_PAD = 8;
				int nPad = ( pLocator->GetIconHeight() / 2 ) + HEIGHT_PAD;

				// Update our position
				if ( m_Signifiers[itr].m_hTargetEntity != NULL )
				{
					pLocator->m_vecOrigin = m_Signifiers[itr].m_hTargetEntity->WorldSpaceCenter();

					if ( V_strstr( m_Signifiers[itr].m_hTargetEntity->GetSignifierName(), "button" ) )
					{
						Vector vecBoundsMax, vecBoundsMin;
						m_Signifiers[itr].m_hTargetEntity.Get()->GetRenderBounds( vecBoundsMin, vecBoundsMax );
						pLocator->m_vecOrigin.z += vecBoundsMax.z + 42;
					}
					else
					{
						// We'd actually like to find the size of this entity on the screen and make the indicator float above the target!
						int nObjectHeight = GetObjectScreenHeight( m_Signifiers[itr].m_hTargetEntity.Get() );
						int nHeightOffset = ( nObjectHeight / 2 );
						pLocator->m_offsetY = -(nHeightOffset+nPad);
					}
				}
				else
				{
					// don't offset anymore
					/*
					Vector2D mins, maxs;
					UTIL_WorldSpaceToScreensSpaceBounds( m_Signifiers[itr].m_vOrigin, -Vector(8,8,8), Vector(8,8,8), &mins, &maxs );

					int nHeightOffset = abs(maxs.y - mins.y);
					pLocator->m_offsetY = -(nHeightOffset+nPad);
					*/
				}

				pLocator->Update();
				
				// Fade away
				if ( bDead || m_Signifiers[itr].m_flLifetime < gpGlobals->curtime || ( m_Signifiers[itr].m_hTargetEntity == NULL && m_Signifiers[itr].m_vOrigin == vec3_invalid ) )
				{
					// Remove the glow
					if ( m_Signifiers[itr].m_nGlowIndex != -1 )
					{
						g_GlowObjectManager.UnregisterGlowObject( m_Signifiers[itr].m_nGlowIndex );
					}

					Locator_RemoveTarget( m_Signifiers[itr].m_nLocatorIndex );
					m_Signifiers.FastRemove( itr );
				}
			}
		}
	}

	virtual void LevelShutdownPostEntity()
	{
		// Shut down all glows
		FOR_EACH_VEC( m_Signifiers, itr )
		{
			if ( m_Signifiers[itr].m_nGlowIndex != -1 )
			{
				g_GlowObjectManager.UnregisterGlowObject( m_Signifiers[itr].m_nGlowIndex );
			}
		}

		// Clear the list
		m_Signifiers.Purge();
	}

	// Add an indicator to the world
	void AddLocator( const char *lpszIconName, int nPlayerIndex, C_BaseEntity *pTarget, const Vector &vPosition, float flLifetime, Color rgbaColor, bool bScaleByDistance )
	{
		C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer();
		if ( pLocalPlayer && !pLocalPlayer->IsAlive() )
		{
			return;
		}

		int nRecycledGlowIndex = -1;

		// First, remove any duplicated icons
		FOR_EACH_VEC_BACK( m_Signifiers, itr )
		{
			float flAlpha = 255;
			if ( pTarget )
				flAlpha = pTarget->GetRenderAlpha();

			// We only want one glow handle per entity, so "steal" the glow control away from older locators
			if ( m_Signifiers[itr].m_hTargetEntity && m_Signifiers[itr].m_hTargetEntity == pTarget && m_Signifiers[itr].m_nSplitscreenID == GET_ACTIVE_SPLITSCREEN_SLOT() && flAlpha > 0 )
			{
				nRecycledGlowIndex = m_Signifiers[itr].m_nGlowIndex;
				m_Signifiers[itr].m_nGlowIndex = -1;

				// Kill it
				Locator_RemoveTarget( m_Signifiers[itr].m_nLocatorIndex );
				m_Signifiers.FastRemove( itr );
			}
			else
			{
				CLocatorTarget *pLocator = Locator_GetTargetFromHandle( m_Signifiers[itr].m_nLocatorIndex );
				if ( pLocator )
				{
					if ( m_Signifiers[itr].m_nPlayerIndex == nPlayerIndex && FStrEq( pLocator->GetOnscreenIconTextureName(), lpszIconName ) )
					{
						// Remove the glow
						if ( m_Signifiers[itr].m_nGlowIndex != -1 )
						{
							g_GlowObjectManager.UnregisterGlowObject( m_Signifiers[itr].m_nGlowIndex );
						}

						// Kill it
						Locator_RemoveTarget( m_Signifiers[itr].m_nLocatorIndex );
						m_Signifiers.FastRemove( itr );
					}
				}
			}
		}

		// Now add a fresh one
		int nIndex = Locator_AddTarget();
		CLocatorTarget *pLocatorTarget = Locator_GetTargetFromHandle( nIndex );
		if ( pLocatorTarget )
		{
			pLocatorTarget->m_vecOrigin = ( pTarget != NULL ) ? pTarget->WorldSpaceCenter() : vPosition;
			pLocatorTarget->SetOnscreenIconTextureName( lpszIconName );
			pLocatorTarget->SetOffscreenIconTextureName( lpszIconName );
			pLocatorTarget->AddIconEffects( LOCATOR_ICON_FX_FORCE_CAPTION );	// Draw even when occluded

			if ( bScaleByDistance )
			{
				pLocatorTarget->AddIconEffects( LOCATOR_ICON_FX_SCALE_BY_DIST );
			}
			else
			{
				pLocatorTarget->AddIconEffects( LOCATOR_ICON_FX_SCALE_LARGE );
			}

			pLocatorTarget->SetIconColor( rgbaColor );

			pLocatorTarget->Update();
		}

		// dont glow players
		bool bTargetIsPlayer = ToPortalPlayer( pTarget ) != NULL;

		// Now, add a glow to this entity (unless we're recycling an old one)
		C_BasePlayer *pPingPlayer = UTIL_PlayerByIndex( nPlayerIndex );

		bool bRecycled = ( nRecycledGlowIndex != -1 && !bTargetIsPlayer );

		int nGlowIndex = bRecycled ? nRecycledGlowIndex : AddGlowToObject( pTarget, pPingPlayer ? pPingPlayer->GetTeamNumber() : 0 );

		if ( bRecycled )
		{
			Vector vColor;
			TeamPingColor( pPingPlayer ? pPingPlayer->GetTeamNumber() : 0, vColor );

			g_GlowObjectManager.SetColor( nGlowIndex, vColor );
		}

		// Hold it
		m_Signifiers.AddToTail( SignifierData_t( nIndex, nGlowIndex, nPlayerIndex, GET_ACTIVE_SPLITSCREEN_SLOT(), pTarget, vPosition, flLifetime ) );
	}
	
	CUtlVector<SignifierData_t> m_Signifiers;
};
CSignifierSystem g_SignifierSystem;

//--------------------------------------------------------------------------------------------------------
inline bool IsPortalOnFloor( CBaseEntity *pEntity )
{
	C_Portal_Base2D *pHitPortal = dynamic_cast<C_Portal_Base2D *>(pEntity);
	if ( pHitPortal )
	{
		Vector vForward;
		pHitPortal->GetVectors( &vForward, NULL, NULL );
		return ( vForward[2] > 0.75f );
	}

	return false;
}



//--------------------------------------------------------------------------------------------------------
CEG_NOINLINE void PlaceCommandTargetDecal( const Vector &vPosition, const Vector &vNormal, int iTeam, bool bJustArrows )
{
	// Recreate the trace that got us here
	C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();

	if ( !pPlayer )
		return;

	QAngle angNormal;
	VectorAngles( vNormal, angNormal );
	angNormal.x += 90.0f;

	Color color( 255, 255, 255 );
	if ( iTeam == TEAM_RED )
		color = UTIL_Portal_Color( 2, 0 );  //orange
	else
		color = UTIL_Portal_Color( 1, 0 );  //blue

	Vector vColor;
	vColor.x = color.r();
	vColor.y = color.g();
	vColor.z = color.b();

	if ( bJustArrows )
	{
		DispatchParticleEffect( "command_target_ping_just_arrows", vPosition, vColor, angNormal );
	}
	else
	{
		DispatchParticleEffect( "command_target_ping", vPosition, vColor, angNormal );
	}
}

CEG_PROTECT_FUNCTION( PlaceCommandTargetDecal );

//--------------------------------------------------------------------------------------------------------
void AddLocator( C_BaseEntity *pTarget, const Vector &vPosition, const Vector &vNormal, int nPlayerIndex, const char *caption, float fDisplayTime )
{	
	int nSplitscreenSlot = GET_ACTIVE_SPLITSCREEN_SLOT();

	bool bCountdown = StringHasPrefix( caption, "countdown" );

	if ( fDisplayTime > gpGlobals->curtime )
	{
		if ( bCountdown )
		{
			IGameEvent *event = gameeventmanager->CreateEvent( "player_countdown" );
			if ( event )
			{
				C_BasePlayer *pPlayer = UTIL_PlayerByIndex( nPlayerIndex );
				event->SetInt( "userid", pPlayer ? pPlayer->GetUserID() : 0 );
				gameeventmanager->FireEventClientSide( event );
			}

			// Count down animates
			CUtlVector< SignifierInfo_t > *pSignifierQueue = GetSignifierQueue();

			int nNew = pSignifierQueue->AddToTail();
			SignifierInfo_t *pNewSignifier = &((*pSignifierQueue)[ nNew ]);
			pNewSignifier->hTarget = pTarget;
			pNewSignifier->vPos = vPosition;
			pNewSignifier->vNormal = vNormal;
			pNewSignifier->nPlayerIndex = nPlayerIndex;
			V_strcpy( pNewSignifier->szCaption, "countdown_3" );
			pNewSignifier->fDisplayTime = fDisplayTime;

			nNew = pSignifierQueue->AddToTail();
			pNewSignifier = &((*pSignifierQueue)[ nNew ]);
			pNewSignifier->hTarget = pTarget;
			pNewSignifier->vPos = vPosition;
			pNewSignifier->vNormal = vNormal;
			pNewSignifier->nPlayerIndex = nPlayerIndex;
			V_strcpy( pNewSignifier->szCaption, "countdown_2" );
			pNewSignifier->fDisplayTime = fDisplayTime + 1.0f;

			nNew = pSignifierQueue->AddToTail();
			pNewSignifier = &((*pSignifierQueue)[ nNew ]);
			pNewSignifier->hTarget = pTarget;
			pNewSignifier->vPos = vPosition;
			pNewSignifier->vNormal = vNormal;
			pNewSignifier->nPlayerIndex = nPlayerIndex;
			V_strcpy( pNewSignifier->szCaption, "countdown_1" );
			pNewSignifier->fDisplayTime = fDisplayTime + 2.0f;

			nNew = pSignifierQueue->AddToTail();
			pNewSignifier = &((*pSignifierQueue)[ nNew ]);
			pNewSignifier->hTarget = pTarget;
			pNewSignifier->vPos = vPosition;
			pNewSignifier->vNormal = vNormal;
			pNewSignifier->nPlayerIndex = nPlayerIndex;
			V_strcpy( pNewSignifier->szCaption, "countdown_go" );
			pNewSignifier->fDisplayTime = fDisplayTime + 3.0f;
		}
		else
		{
			// Put it in the queue for later display
			CUtlVector< SignifierInfo_t > *pSignifierQueue = GetSignifierQueue();

			int nNew = pSignifierQueue->AddToTail();
			SignifierInfo_t *pNewSignifier = &((*pSignifierQueue)[ nNew ]);
			pNewSignifier->hTarget = pTarget;
			pNewSignifier->vPos = vPosition;
			pNewSignifier->vNormal = vNormal;
			pNewSignifier->nPlayerIndex = nPlayerIndex;
			V_strcpy( pNewSignifier->szCaption, caption );
			pNewSignifier->fDisplayTime = fDisplayTime;
		}
		return;
	}

	C_BasePlayer *pPlayer = UTIL_PlayerByIndex( nPlayerIndex );
	int nTeamSlot = ( ( pPlayer && pPlayer->GetTeamNumber() == TEAM_BLUE ) ? ( 0 ) : ( 1 ) );

	float *pfLastPingTime = &( CRadialMenu::m_fLastPingTime[ nSplitscreenSlot ][ nTeamSlot ] );
	int *pfNumPings = &( CRadialMenu::m_nNumPings[ nSplitscreenSlot ][ nTeamSlot ] );

	if ( !bCountdown )
	{
		// Reduce the current ping count for ones that have faded by now
		float fNextPingTime = *pfLastPingTime + PING_DELAY_BASE + PING_DELAY_INCREMENT * *pfNumPings;
		float fPastNextPingTime = gpGlobals->curtime - fNextPingTime;

		if ( gpGlobals->curtime > *pfLastPingTime && fPastNextPingTime < 0.0f )
		{
			// They've been spamming pings, don't let them place another for now
			return;
		}
		else if ( *pfNumPings > 0 )
		{
			// Pop off old pings that last about 2.5 seconds
			float fOldestPingFadeTime = *pfLastPingTime + 2.5f;

			for ( int i = 1; i <= *pfNumPings; ++i )
			{
				fOldestPingFadeTime -= i * PING_DELAY_INCREMENT;
			}

			float fTimePastOldestFaded = gpGlobals->curtime - fOldestPingFadeTime;

			while ( *pfNumPings > 0 && fTimePastOldestFaded > 0.0f )
			{
				fTimePastOldestFaded -= *pfNumPings * PING_DELAY_INCREMENT;
				(*pfNumPings)--;
			}
		}
	}

	*pfLastPingTime = gpGlobals->curtime;
	(*pfNumPings)++;

	const char *lpszCommand = caption;
	char szIconName[MAX_PATH];
	bool bAddDecal = false;
	bool bColor = true;
	bool bScaleByDistance = true;

	char szSound[ 32 ];
	V_strncpy( szSound, PING_SOUND_NAME, sizeof( szSound ) );

	if ( FStrEq( lpszCommand, "look" ) )
	{
		V_strncpy( szIconName, "icon_look", sizeof(szIconName) );
		bAddDecal = true;
	}
	else if ( FStrEq( lpszCommand, "death_blue" ) )
	{
		V_strncpy( szIconName, "icon_death_blue", sizeof(szIconName) );
		bColor = false;
		bScaleByDistance = false;
		szSound[ 0 ] = '\0';
	}
	else if ( FStrEq( lpszCommand, "death_orange" ) )
	{
		V_strncpy( szIconName, "icon_death_orange", sizeof(szIconName) );
		bColor = false;
		bScaleByDistance = false;
		szSound[ 0 ] = '\0';
	}
	else if ( FStrEq( lpszCommand, "countdown_3" ) )
	{
		V_strncpy( szIconName, "icon_countdown_3", sizeof(szIconName) );
		bScaleByDistance = false;
		V_strncpy( szSound, PING_SOUND_NAME_LOW, sizeof( szSound ) );
	}
	else if ( FStrEq( lpszCommand, "countdown_2" ) )
	{
		V_strncpy( szIconName, "icon_countdown_2", sizeof(szIconName) );
		bScaleByDistance = false;
		V_strncpy( szSound, PING_SOUND_NAME_LOW, sizeof( szSound ) );
	}
	else if ( FStrEq( lpszCommand, "countdown_1" ) )
	{
		V_strncpy( szIconName, "icon_countdown_1", sizeof(szIconName) );
		bScaleByDistance = false;
		V_strncpy( szSound, PING_SOUND_NAME_LOW, sizeof( szSound ) );
	}
	else if ( FStrEq( lpszCommand, "countdown_go" ) )
	{
		V_strncpy( szIconName, "icon_countdown_go", sizeof(szIconName) );
		bScaleByDistance = false;
		V_strncpy( szSound, PING_SOUND_NAME_HIGH, sizeof( szSound ) );
	}
	else if ( FStrEq( lpszCommand, "move_here" ) )
	{
		V_strncpy( szIconName, "icon_move_here", sizeof(szIconName) );
		bAddDecal = true;
	}
	else if ( FStrEq( lpszCommand, "portal_place_floor" ) )
	{
		V_strncpy( szIconName, "icon_portal_place_floor", sizeof(szIconName) );
		bAddDecal = true;
	}
	else if ( FStrEq( lpszCommand, "portal_place_wall" ) )
	{
		V_strncpy( szIconName, "icon_portal_place_wall", sizeof(szIconName) );
		bAddDecal = true;
	}
	else if ( FStrEq( lpszCommand, "portal_move" ) )
	{
		V_strncpy( szIconName, "icon_portal_move", sizeof(szIconName) );
	}
	else if ( FStrEq( lpszCommand, "portal_enter" ) )
	{
		if ( IsPortalOnFloor( pTarget ) )
		{
			V_strncpy( szIconName, "icon_portal_enter_floor", sizeof(szIconName) );
		}
		else
		{
			V_strncpy( szIconName, "icon_portal_enter_wall", sizeof(szIconName) );
		}
	}
	else if ( FStrEq( lpszCommand, "portal_exit" ) )
	{
		if ( IsPortalOnFloor( pTarget ) )
		{
			V_strncpy( szIconName, "icon_portal_exit_floor", sizeof(szIconName) );
		}
		else
		{
			V_strncpy( szIconName, "icon_portal_exit_wall", sizeof(szIconName) );
		}
	}
	else if ( FStrEq( lpszCommand, "slime_no_drink" ) )
	{
		V_strncpy( szIconName, "icon_slime_no_drink", sizeof(szIconName) );
	}
	else if ( FStrEq( lpszCommand, "box_pickup" ) )
	{
		V_strncpy( szIconName, "icon_box_pickup", sizeof(szIconName) );
	}
	else if ( FStrEq( lpszCommand, "box_putdown" ) )
	{
		V_strncpy( szIconName, "icon_box_drop", sizeof(szIconName) );
		bAddDecal = true;
	}
	else if ( FStrEq( lpszCommand, "button_press" ) )
	{
		V_strncpy( szIconName, "icon_button_press", sizeof(szIconName) );
	}
	else if ( FStrEq( lpszCommand, "button_tall_press" ) )
	{
		V_strncpy( szIconName, "icon_button_tall_press", sizeof(szIconName) );
	}
	else if ( FStrEq( lpszCommand, "turret_warning" ) )
	{
		V_strncpy( szIconName, "icon_turret_warning", sizeof(szIconName) );
	}
	else if ( FStrEq( lpszCommand, "love_it" ) )
	{
		V_strncpy( szIconName, "icon_love_it", sizeof( szIconName ) );
		bAddDecal = true;
	}
	else if ( FStrEq( lpszCommand, "stuck" ) )
	{
		V_strncpy( szIconName, "icon_stuck", sizeof( szIconName ) );
		bAddDecal = true;
	}
	else if ( FStrEq( lpszCommand, "hate_it" ) )
	{
		V_strncpy( szIconName, "icon_hate_it", sizeof( szIconName ) );
		bAddDecal = true;
	}
	else if ( FStrEq( lpszCommand, "confused" ) )
	{
		V_strncpy( szIconName, "icon_confused", sizeof( szIconName ) );
		bAddDecal = true;
	}
	else if ( FStrEq( lpszCommand, "done" ) )
	{
		return;
	}
	else
	{
		// Found an unknown command!
		Assert(0);
		return;
	}

	C_Team *pTeam = pPlayer ? pPlayer->GetTeam() : NULL;

	// Add a decal down where we pointed
	if ( bAddDecal )
	{
		bool bJustArrows = false;
		if ( pTarget && V_strstr( pTarget->GetSignifierName(), "button" ) )
		{
			bJustArrows = true;
		}

		int iTeam = pTeam ? pTeam->GetTeamNumber() : 0;
		PlaceCommandTargetDecal( vPosition, vNormal, iTeam, bJustArrows );
	}

	Color color( 255, 255, 255, 255 );

	if ( pPlayer )
	{
		if ( szSound[ 0 ] != '\0' )
		{
			pPlayer->EmitSound( szSound );
		}

		if ( bColor )
		{
			if ( pTeam && pTeam->GetTeamNumber() == TEAM_RED )
			{
				color = UTIL_Portal_Color( 2, 0 );  //orange
			}
			else
			{
				color = UTIL_Portal_Color( 1, 0 );  //blue
			}
		}
	}

	// if single player playtest, our alpha will come back 0, push it up
	if ( color.a() == 0 )
	{
		color.SetColor( color.r(), color.g(), color.b(), 255 );
	}

	float flLifetime = gpGlobals->curtime + ( bCountdown ? 1.0f : 3.0f );
	Vector vecSigPos = vPosition;
	vecSigPos += vNormal*64;
	g_SignifierSystem.AddLocator( szIconName, nPlayerIndex, pTarget, vecSigPos, flLifetime, color, bScaleByDistance );
}

//--------------------------------------------------------------------------------------------------------
static void __MsgFunc_AddLocator( bf_read &msg )
{
	// Find the index of the sending player
	int nPlayerIndex = msg.ReadShort();

	// Find the entity in question
	C_BaseEntity *pTarget = UTIL_EntityFromUserMessageEHandle( msg.ReadLong() );

	float fDisplayTime = msg.ReadFloat();
	
	Vector vPosition = vec3_invalid;
	Vector vNormal = vec3_invalid;
	msg.ReadBitVec3Coord( vPosition );
	msg.ReadBitVec3Normal( vNormal );

	// Find the name of the icon to show
	char iconName[2048]; 
	msg.ReadString( iconName, sizeof(iconName) );
	AddLocator( pTarget, vPosition, vNormal, nPlayerIndex, iconName, fDisplayTime );
}

USER_MESSAGE_REGISTER( AddLocator );