Class UI_Scoreboard extends KFGUI_Page;

var editinline export KFGUI_RightClickMenu PlayerContext;
var KFGUI_List PlayersList;
var() float ScoreboardSpacing;
var color SBTextColor;
var Texture2D HealthIcon;
var KFGameReplicationInfo KFGRI;
var array<ExtPlayerReplicationInfo> PRIList;
var ExtPlayerReplicationInfo RightClickPlayer;
var int PlayerIndex;
var Font SBFont;
var transient float SBFontSize,CashXPos,KillsXPos,AssistXPos,PingXPos,SBFontHeight;
var transient int InitAdminSize;

var KFGUI_Tooltip ToolTipItem;

var transient bool bHasSelectedPlayer,bMeAdmin,bShowSpectatorsOnly;

var KFGUI_Button SpecButton;

var localized string TimeText;
var localized string PlayersText;
var localized string PlayerText;
var localized string AliveText;
var localized string SpectatorsText;
var localized string HeaderWaveText;
var localized string HeaderPlayerText;
var localized string HeaderDoshText;
var localized string HeaderKillsText;
var localized string HeaderAssistText;
var localized string HeaderPingText;
var localized string NoPerkText;
var localized string RespawnText;
var localized string BotText;
var localized string DeadText;
var localized string UnmutePlayerText;
var localized string MutePlayerText;
var localized string YouveMutedText;
var localized string YouveUnmutedText;
var localized string HealthText;
var localized string RClickForOptsText;
var localized string SpecButtonText;
var localized string SpecButtonTooltip;
var localized string SpectateThisPlayerText;
var localized string ViewPlayerProfileText;
var localized string MuteText;

function FRowItem NewFRowItem(string Text, bool isSplitter)
{
	local FRowItem newItem;
	newItem.Text=Text;
	newItem.bSplitter=isSplitter;
	return newItem;
}

function InitMenu()
{
	Super.InitMenu();
	HealthIcon = Texture2D(DynamicLoadObject("UI_Objective_Tex.UI_Obj_Healing_Loc",class'Texture2D'));
	PlayersList = KFGUI_List(FindComponentID('PlayerList'));
	SpecButton = KFGUI_Button(FindComponentID('Spec'));
	
	SpecButton.ButtonText=SpecButtonText;
	SpecButton.Tooltip=SpecButtonTooltip;
	
	PlayerContext.ItemRows.AddItem(NewFRowItem(SpectateThisPlayerText, false));
	PlayerContext.ItemRows.AddItem(NewFRowItem(ViewPlayerProfileText, false));
	PlayerContext.ItemRows.AddItem(NewFRowItem(MuteText, false));
	PlayerContext.ItemRows.AddItem(NewFRowItem("", true));
}

function ShowMenu()
{
	local ExtPlayerController PC;
	local int i;
	local bool bAdmin;

	Owner.bAbsorbInput = false;
	PC = ExtPlayerController(GetPlayer());
	bAdmin = PC!=None && (PC.WorldInfo.NetMode!=NM_Client || (PC.PlayerReplicationInfo!=None && PC.PlayerReplicationInfo.bAdmin));
	if( PC!=None && (InitAdminSize!=PC.AdminCommands.Length || !bAdmin) )
	{
		InitAdminSize = (bAdmin ? PC.AdminCommands.Length : 0);
		PlayerContext.ItemRows.Length = 4+InitAdminSize;
		for( i=0; i<InitAdminSize; ++i )
			PlayerContext.ItemRows[4+i].Text = PC.AdminCommands[i].Info;
	}
}
function CloseMenu()
{
	Owner.bAbsorbInput = true;
	KFGRI = None;
	PRIList.Length = 0;
	RightClickPlayer = None;
	bHasSelectedPlayer = false;
	bShowSpectatorsOnly = false;
}

function bool InOrder( PlayerReplicationInfo A, PlayerReplicationInfo B )
{
	if( A.bOnlySpectator!=B.bOnlySpectator )
		return B.bOnlySpectator;
	if( A.Kills!=B.Kills )
		return (A.Kills<B.Kills);
	if( A.Score!=B.Score )
		return (A.Score<B.Score);
	return (A.PlayerName<B.PlayerName);
}
function DrawMenu()
{
	local PlayerController PC;
	local float XPos,YPos,XScale,YHeight,Y,FontScalar,BoxHeight,XL,YL;
	local int i,j,NumSpec,NumPlayer,NumAlivePlayer;
	local PlayerReplicationInfo PRI;
	local ExtPlayerReplicationInfo KPRI;
	local byte DefFont;
	
	PC = GetPlayer();
	if( KFGRI==None )
	{
		KFGRI = KFGameReplicationInfo(PC.WorldInfo.GRI);
		if( KFGRI==None )
			return;
	}
	bMeAdmin = (PC.WorldInfo.NetMode!=NM_Client || (ExtPlayerReplicationInfo(PC.PlayerReplicationInfo)!=None && ExtPlayerReplicationInfo(PC.PlayerReplicationInfo).AdminType<=1));

	// Sort player list.
	for( i=(KFGRI.PRIArray.Length-1); i>0; --i )
	{
		for( j=i-1; j>=0; --j )
			if( !InOrder(KFGRI.PRIArray[i],KFGRI.PRIArray[j]) )
			{
				PRI = KFGRI.PRIArray[i];
				KFGRI.PRIArray[i] = KFGRI.PRIArray[j];
				KFGRI.PRIArray[j] = PRI;
			}
	}
	
	// Check players.
	PlayerIndex = -1;
	NumPlayer = 0;
	for( i=(KFGRI.PRIArray.Length-1); i>=0; --i )
	{
		KPRI = ExtPlayerReplicationInfo(KFGRI.PRIArray[i]);
		if( KPRI==None || KPRI.bHiddenUser )
			continue;
		if( KPRI.bOnlySpectator )
		{
			++NumSpec;
			continue;
		}
		if( KPRI.PlayerHealth>0 && KPRI.PlayerHealthPercent>0 && KPRI.GetTeamNum()==0 )
			++NumAlivePlayer;
		++NumPlayer;
	}
	
	PRIList.Length = (bShowSpectatorsOnly ? NumSpec : NumPlayer);
	j = PRIList.Length;
	for( i=(KFGRI.PRIArray.Length-1); i>=0; --i )
	{
		KPRI = ExtPlayerReplicationInfo(KFGRI.PRIArray[i]);
		if( KPRI!=None && bShowSpectatorsOnly==KPRI.bOnlySpectator )
		{
			PRIList[--j] = KPRI;
			if( KPRI==PC.PlayerReplicationInfo )
				PlayerIndex = j;
		}
	}

	// Header font info.
	DefFont = Owner.CurrentStyle.DefaultFontSize;
	Canvas.Font = Owner.CurrentStyle.PickFont(DefFont,FontScalar);
	YL = Owner.CurrentStyle.DefaultHeight;
	YHeight = YL*5.f;
	
	// Draw header.
	if( Canvas.ClipX<1000 )
	{
		XPos = Canvas.ClipX*0.2;
		XScale = Canvas.ClipX*0.6;
	}
	else
	{
		XPos = Canvas.ClipX*0.3;
		XScale = Canvas.ClipX*0.4;
	}
	YPos = Canvas.ClipY*0.05;
	Canvas.SetDrawColor(128,32,32,FrameOpacity);
	Owner.CurrentStyle.DrawRectBox(XPos,YPos,XScale*0.75,YHeight,26,1);

	Canvas.DrawColor = SBTextColor;

	Y = YPos+10;
	Canvas.SetPos(XPos+26,Y);
	Canvas.DrawText(PC.WorldInfo.Title,,FontScalar,FontScalar);

	Y+=YL;
	Canvas.SetPos(XPos+26,Y);
	Canvas.DrawText(KFGRI.ServerName,,FontScalar,FontScalar);

	if( KFGRI.GameClass!=None )
	{
		Y+=YL;
		Canvas.SetPos(XPos+26,Y);
		Canvas.DrawText(KFGRI.GameClass.Default.GameName$" - "$Class'KFCommon_LocalizedStrings'.Static.GetDifficultyString(KFGRI.GameDifficulty),,FontScalar,FontScalar);
	}

	Y+=YL;
	Canvas.SetPos(XPos+26,Y);
	Canvas.DrawText(TimeText$": "$FormatTimeSM(KFGRI.ElapsedTime)$" | "$PlayersText$": "$NumPlayer$" | "$AliveText$": "$NumAlivePlayer$" | "$SpectatorsText$": "$NumSpec,,FontScalar,FontScalar);
	
	XPos += XScale*0.75-1;
	XScale *= 0.25;
	Canvas.SetDrawColor(18,8,8,FrameOpacity);
	Owner.CurrentStyle.DrawRectBox(XPos,YPos,XScale,YHeight,26);
	Canvas.DrawColor = SBTextColor;
	Canvas.Font = Owner.CurrentStyle.PickFont(DefFont+3,FontScalar);
	Canvas.TextSize("A",XL,YL,FontScalar,FontScalar);
	Y = YPos+4;
	DrawCenteredText(HeaderWaveText,XPos+XScale*0.5,Y,FontScalar);
	Y += YL;
	DrawCenteredText(KFGRI.WaveNum$"/"$(KFGRI.WaveMax-1),XPos+XScale*0.5,Y,FontScalar*1.1);
	
	// Scoreboard title line.
	Canvas.Font = Owner.CurrentStyle.PickFont(DefFont,FontScalar);
	YL = Owner.CurrentStyle.DefaultHeight;
	if( Canvas.ClipX<1000 )
	{
		XPos = Canvas.ClipX*0.175;
		XScale = Canvas.ClipX*0.65;
	}
	else
	{
		XPos = Canvas.ClipX*0.25;
		XScale = Canvas.ClipX*0.5;
	}
	YPos += YHeight*1.05;
	YHeight = YL;
	if( bShowSpectatorsOnly )
		Canvas.SetDrawColor(32,32,128,FrameOpacity);
	else Canvas.SetDrawColor(128,32,32,FrameOpacity);
	Owner.CurrentStyle.DrawRectBox(XPos,YPos,XScale,YHeight,16,2);
	
	// Calc X offsets
	CashXPos = XScale*0.5;
	KillsXPos = XScale*0.7;
	AssistXPos = XScale*0.8;
	PingXPos = XScale*0.9;
	
	// Header texts
	Canvas.DrawColor = SBTextColor;
	Y = YPos+4;
	Canvas.SetPos(XPos+18,Y);
	Canvas.DrawText(HeaderPlayerText,,FontScalar,FontScalar);
	if( !bShowSpectatorsOnly )
	{
		Canvas.SetPos(XPos+CashXPos,Y);
		Canvas.DrawText(HeaderDoshText,,FontScalar,FontScalar);
		DrawCenteredText(HeaderKillsText,XPos+KillsXPos,Y,FontScalar);
		DrawCenteredText(HeaderAssistText,XPos+AssistXPos,Y,FontScalar);
	}
	DrawCenteredText(HeaderPingText,XPos+PingXPos,Y,FontScalar);

	// Check how many players to draw.
	YPos+=(YHeight-1);
	YHeight = (Canvas.ClipY*0.95) - YPos;
	i = DefFont+2;
	while( i>0 )
	{
		Canvas.Font = Owner.CurrentStyle.PickFont(i,SBFontSize);
		Canvas.TextSize("A",XL,SBFontHeight,SBFontSize,SBFontSize);
		BoxHeight = SBFontHeight*2.f+ScoreboardSpacing;
		if( (BoxHeight*PRIList.Length)<=YHeight )
			break;
		--i;
	}
	
	// Scoreboard background.
	Canvas.SetDrawColor(18,8,8,FrameOpacity);
	Owner.CurrentStyle.DrawRectBox(XPos,YPos,XScale,YHeight,16);
	
	// Setup listing.
	PlayersList.XPosition = (XPos+8.f) / InputPos[2];
	PlayersList.YPosition = (YPos+8.f) / InputPos[3];
	PlayersList.XSize = (XScale-16.f) / InputPos[2];
	PlayersList.YSize = (YHeight-16.f) / InputPos[3];
	PlayersList.ListItemsPerPage = YHeight/BoxHeight;
	PlayersList.ChangeListSize(PRIList.Length);
	SBFont = Canvas.Font;
}

final function Texture2D FindAvatar( UniqueNetId ClientID )
{
	local string S;
	
	S = KFPlayerController(GetPlayer()).GetSteamAvatar(ClientID);
	if( S=="" )
		return None;
	return Texture2D(FindObject(S,class'Texture2D'));
}

final function DrawCenteredText( string S, float X, float Y, optional float Scale=1.f )
{
	local float XL,YL;

	Canvas.TextSize(S,XL,YL);
	Canvas.SetPos(X-(XL*Scale*0.5),Y);
	Canvas.DrawText(S,,Scale,Scale);
}
static final function string FormatTimeSM( float Sec )
{
	local int Seconds,Minutes;

	Sec = Abs(Sec);
	Seconds = int(Sec);
	Minutes = Seconds/60;
	Seconds-=Minutes*60;

	return Minutes$":"$(Seconds<10 ? "0"$Seconds : string(Seconds));
}

function DrawPlayerEntry( Canvas C, int Index, float YOffset, float Height, float Width, bool bFocus )
{
	local ExtPlayerReplicationInfo PRI;
	local float XPos,YPos,XL,YL;
	local string S;
	
	if( Index==0 )
	{
		// Setup font info.
		C.Font = SBFont;
	}
	PRI = PRIList[Index];
	
	bFocus = bFocus || (bHasSelectedPlayer && RightClickPlayer==PRI);
	
	// Draw name entry background.
	if( PRI.bOnlySpectator ) // Spectator - blue.
	{
		if( bFocus )
			C.SetDrawColor(86,86,212,FrameOpacity);
		else C.SetDrawColor(48,48,164,FrameOpacity);
	}
	else if( PRI.Team==None ) // Unteamed - Grey.
	{
		if( bFocus )
			C.SetDrawColor(86,86,86,FrameOpacity);
		else C.SetDrawColor(48,48,48,FrameOpacity);
	}
	else
	{
		switch( PRI.Team.TeamIndex )
		{
		case 0: // Humans - Red.
			if( bFocus )
				C.SetDrawColor(160,48,48,FrameOpacity);
			else C.SetDrawColor(128,32,32,FrameOpacity);
			break;
		default: // Rest - Green.
			if( bFocus )
				C.SetDrawColor(48,160,48,FrameOpacity);
			else C.SetDrawColor(32,128,32,FrameOpacity);
		}
	}
	if( PRI.PlayerHealth<=0 || PRI.PlayerHealthPercent<=0 )
		C.DrawColor = C.DrawColor*0.6;
	Owner.CurrentStyle.DrawRectBox(0.f,YOffset,Width,Height-ScoreboardSpacing,10);
	Height-=ScoreboardSpacing;
	
	// Draw health bg.
	if( !bShowSpectatorsOnly )
	{
		if( PRI.PlayerHealth<30 || PRI.PlayerHealthPercent<=0 ) // Chose color based on health.
			C.SetDrawColor(220,32,32,255);
		else if( PRI.PlayerHealth<70 )
			C.SetDrawColor(220,220,32,255);
		else C.SetDrawColor(32,225,32,255);
		Owner.CurrentStyle.DrawRectBox(6.f,YOffset+6,Height-12,Height-12,5);
	}

	// Avatar
	if( PRI.Avatar!=None )
	{
		C.SetDrawColor(255,255,255,255);
		C.SetPos(Height+4,YOffset+4);
		C.DrawTile(PRI.Avatar,Height-8,Height-8,0,0,PRI.Avatar.SizeX,PRI.Avatar.SizeY);
		XPos = Height*2+8;
	}
	else
	{
		XPos = Height+4;

		// Try to obtain avatar.
		if( !PRI.bBot )
			PRI.Avatar = FindAvatar(PRI.UniqueId);
	}
	
	// Name
	C.SetPos(XPos,YOffset+2);
	if( PlayerIndex==Index )
		C.SetDrawColor(128,255,128,255);
	else C.DrawColor = SBTextColor;
	YPos = SBFontSize;
	S = PRI.TaggedPlayerName;
	if( PRI.ShowAdminName() )
	{
		S = S$" ("$PRI.GetAdminNameAbr()$")";
		C.DrawColor = PRI.GetAdminColorC();
	}
	else if( PRI.bIsDev )
	{
		S = S$" (D)";
		C.DrawColor = MakeColor(130,255,235,255);
	}
	if( bMeAdmin && PRI.FixedData>0 )
	{
		C.DrawColor = MakeColor(255,0,0,255);
		S = S$" -"$PRI.GetDesc();
	}
	while( true ) // Make sure too long name doesn't overleap.
	{
		C.TextSize(S,XL,YL,YPos,YPos);
		if( (C.CurX+XL)<CashXPos )
			break;
		YPos*=0.9;
	}
	C.DrawText(S,,YPos,YPos);
	
	// Other info background.
	C.SetDrawColor(4,4,4,255);
	Owner.CurrentStyle.DrawRectBox(CashXPos-4,YOffset+4,Width-CashXPos-8,Height-8,6);
	
	// Perk
	if( !bShowSpectatorsOnly )
	{
		if( PRI.ECurrentPerk!=None )
		{
			// Icon.
			C.DrawColor = PRI.HUDPerkColor;
			C.SetPos(XPos,YOffset+Height*0.5);
			C.DrawRect(Height*0.475,Height*0.475,PRI.ECurrentPerk.Default.PerkIcon);

			// Name.
			S = PRI.GetPerkLevelStr()@PRI.ECurrentPerk.Default.PerkName;
		}
		else if( PRI.bBot && PRI.CurrentPerkClass!=None )
		{
			// Icon.
			C.DrawColor = SBTextColor;
			C.SetPos(XPos,YOffset+Height*0.5);
			C.DrawRect(Height*0.475,Height*0.475,PRI.CurrentPerkClass.Default.PerkIcon);

			// Name.
			S = PRI.CurrentPerkClass.Default.PerkName;
		}
		else
		{
			C.DrawColor = SBTextColor;
			S = NoPerkText;
		}
		YPos = SBFontSize*0.9;
		C.SetPos(XPos+Height*0.5,YOffset+Height*0.495);
		if( PRI.RespawnCounter>=0 )
		{
			C.DrawColor = SBTextColor;
			S = RespawnText$": "$FormatTimeSM(PRI.RespawnCounter);
		}
		while( true ) // Make sure too long name doesn't overleap.
		{
			C.TextSize(S,XL,YL,YPos,YPos);
			if( (C.CurX+XL)<CashXPos )
				break;
			YPos*=0.8;
		}
		C.DrawText(S,,YPos,YPos);
	}
	
	// Cash
	C.DrawColor = SBTextColor;
	YPos = YOffset+(Height-SBFontHeight)*0.5;
	if( !bShowSpectatorsOnly )
	{
		C.SetPos(CashXPos,YPos);
		C.DrawText(string(int(PRI.Score)),,SBFontSize,SBFontSize);
		
		// Kills
		DrawCenteredText(string(PRI.Kills),KillsXPos,YPos,SBFontSize);
		
		// Assists
		DrawCenteredText(string(PRI.Assists),AssistXPos,YPos,SBFontSize);
	}
		
	// Ping
	DrawCenteredText(PRI.bBot ? BotText : string(PRI.Ping*4),PingXPos,YPos,SBFontSize);
	
	// Draw health.
	if( !bShowSpectatorsOnly )
	{
		if( HealthIcon!=None )
		{
			C.SetPos(6+(Height-12)*0.25,YOffset+8);
			C.DrawTile(HealthIcon,(Height-12)*0.5,(Height-12)*0.5,0,0,256,256);
		}
		if( PRI.PlayerHealth<=0 || PRI.PlayerHealthPercent<=0 )
			DrawCenteredText(DeadText,6+(Height-12)*0.5,YOffset+Height*0.45,SBFontSize*0.95);
		else DrawCenteredText(string(PRI.PlayerHealth),6+(Height-12)*0.5,YOffset+Height*0.45,SBFontSize*0.95);
	}
}
function ClickedPlayer( int Index, bool bRight, int MouseX, int MouseY )
{
	local PlayerController PC;
	local int i;

	if( !bRight || Index<0 )
		return;
	bHasSelectedPlayer = true;
	RightClickPlayer = PRIList[Index];
	
	// Check what items to disable.
	PC = GetPlayer();
	PlayerContext.ItemRows[0].bDisabled = (PlayerIndex==Index || !PC.IsSpectating());
	PlayerContext.ItemRows[1].bDisabled = RightClickPlayer.bBot;
	PlayerContext.ItemRows[2].bDisabled = (PlayerIndex==Index || RightClickPlayer.bBot);
	PlayerContext.ItemRows[2].Text = (PlayerContext.ItemRows[2].bDisabled || PC.IsPlayerMuted(RightClickPlayer.UniqueId)) ? UnmutePlayerText : MutePlayerText;

	if( PlayerIndex==Index ) // Selected self.
	{
		for( i=4; i<PlayerContext.ItemRows.Length; ++i )
			PlayerContext.ItemRows[i].bDisabled = true;
	}
	else
	{
		for( i=4; i<PlayerContext.ItemRows.Length; ++i )
			PlayerContext.ItemRows[i].bDisabled = false;
	}

	PlayerContext.OpenMenu(Self);
}
function HidRightClickMenu( KFGUI_RightClickMenu M )
{
	bHasSelectedPlayer = false;
}
function SelectedRCItem( int Index )
{
	local PlayerController PC;

	PC = GetPlayer();
	switch( Index )
	{
	case 0: // Spectate this player.
		PC.ConsoleCommand("ViewPlayerID "$RightClickPlayer.PlayerID);
		break;
	case 1: // Steam profile.
		OnlineSubsystemSteamworks(class'GameEngine'.static.GetOnlineSubsystem()).ShowProfileUI(0,,RightClickPlayer.UniqueId);
		break;
	case 2: // Mute voice.
		if( !PC.IsPlayerMuted(RightClickPlayer.UniqueId) )
		{
			PC.ClientMessage(YouveMutedText$" "$RightClickPlayer.TaggedPlayerName);
			PC.ClientMutePlayer(RightClickPlayer.UniqueId);
			RightClickPlayer.bIsMuted = true;
		}
		else
		{
			PC.ClientMessage(YouveUnmutedText$" "$RightClickPlayer.TaggedPlayerName);
			PC.ClientUnmutePlayer(RightClickPlayer.UniqueId);
			RightClickPlayer.bIsMuted = false;
		}
		break;
	default:
		if( Index>=4 )
			PC.ConsoleCommand("Admin "$ExtPlayerController(PC).AdminCommands[Index-4].Cmd@RightClickPlayer.PlayerID);
	}
}

function ShowPlayerTooltip( int Index )
{
	local ExtPlayerReplicationInfo PRI;
	local string S;
	
	PRI = PRIList[Index];
	if( PRI!=None )
	{
		if( ToolTipItem==None )
		{
			ToolTipItem = New(None)Class'KFGUI_Tooltip';
			ToolTipItem.Owner = Owner;
			ToolTipItem.ParentComponent = Self;
			ToolTipItem.InitMenu();
		}
		S = PlayerText$": "$PRI.TaggedPlayerName$"|"$HealthText$": "$(PRI.PlayerHealthPercent<=0 ? "0" : string(PRI.PlayerHealth));
		if( PRI.ShowAdminName() )
			S = S$"|"$PRI.GetAdminName();
		S = S$"|"$RClickForOptsText;
		ToolTipItem.SetText(S);
		ToolTipItem.ShowMenu();
		ToolTipItem.CompPos[0] = Owner.MousePosition.X;
		ToolTipItem.CompPos[1] = Owner.MousePosition.Y;
		ToolTipItem.GetInputFocus();
	}
}

function ButtonClicked( KFGUI_Button Sender )
{
	switch( Sender.ID )
	{
	case 'Spec':
		bShowSpectatorsOnly = !bShowSpectatorsOnly;
		break;
	}
}

defaultproperties
{
	bAlwaysTop=true
	SBTextColor=(R=250,G=250,B=250,A=255)
	ScoreboardSpacing=4
	
	Begin Object Class=KFGUI_List Name=PlayerList
		bDrawBackground=false
		OnDrawItem=DrawPlayerEntry
		OnClickedItem=ClickedPlayer
		ID="PlayerList"
		bClickable=true
		OnMouseRest=ShowPlayerTooltip
	End Object
	Begin Object Class=KFGUI_Button Name=B_ShowSpecs
		ID="Spec"
		XPosition=0.67
		YPosition=0.95
		XSize=0.09
		YSize=0.03
		OnClickLeft=ButtonClicked
		OnClickRight=ButtonClicked
	End Object
	
	Components.Add(PlayerList)
	Components.Add(B_ShowSpecs)
	
	Begin Object Class=KFGUI_RightClickMenu Name=PlayerContextMenu
		OnSelectedItem=SelectedRCItem
		OnBecameHidden=HidRightClickMenu
	End Object
	PlayerContext=PlayerContextMenu
}