// ====================================================================
// (C) 2002, Epic Games
// ====================================================================

class ExtendedConsole extends Console;

#exec OBJ LOAD FILE=ROMenuSounds.uax
#exec OBJ LOAD FILE=KF_InterfaceArt_tex.utx
// if _RO_
// else
//#exec OBJ LOAD FILE=InterfaceContent.utx
// end if _RO_

// Visible Console stuff
var globalconfig int MaxScrollbackSize;

var array<string> Scrollback;
var int SBHead, SBPos;	// Where in the scrollback buffer are we
var bool bCtrl, bAlt, bShift;
var bool bConsoleHotKey;

var float   ConsoleSoundVol;

var localized string AddedCurrentHead;
var localized string AddedCurrentTail;

var localized string ServerFullMsg;

////// Speech Menu
var float SMLineSpace;

var enum ESpeechMenuState
{
	SMS_Main,
	SMS_VoiceChat,			// List of voice chat groups on the server
	SMS_Ack,
	SMS_FriendFire,
	SMS_Order,
	SMS_Other,
	SMS_Taunt,
	SMS_TauntAnim,
	SMS_PlayerSelect,
	SMS_VoiceChatChannel	// List of options for this channel (public & private)
} SMState;

// These are put together using the menu and sent to the 'Speech' exec command.
var name SMType;
var int  SMIndex;
var string SMCallsign;

var int SMOffset;

var string	SMNameArray[48];
var int		SMIndexArray[48];
var int		SMArraySize;

var config float SMOriginX;
var config float SMOriginY;
var float SMMargin, SMTab;

// if _RO_
var localized 	string  SMStateName[20];
// else
// var localized 	string  SMStateName[10];
// end if _RO_
var localized 	string  SMChannelOptions[3];
var				array<VoiceChatRoom> VoiceChannels;
var localized 	string  SMAllString;
var localized 	string  SMMoreString;

var sound	SMOpenSound;
var sound   SMAcceptSound;
var sound   SMDenySound;

var config EInputKey	LetterKeys[10];
var        EInputKey	NumberKeys[10];

var config bool bSpeechMenuUseLetters;
var config bool bSpeechMenuUseMouseWheel;
var bool		bSpeechMenuLocked;

var int HighlightRow;

////// End Speech Menu

struct StoredPassword
{
	var config string	Server,
						Password;
};

struct ServerFavorite
{
	var() config int ServerID;
	var() config string IP;
	var() config int Port;
	var() config int QueryPort;
	var() config string ServerName;
};

var() protected config array<ServerFavorite> Favorites;

var config array<StoredPassword>	SavedPasswords;
var config string					PasswordPromptMenu;
var string							LastConnectedServer,
									LastURL;

struct ChatStruct
{
	var string	Message;
    var int		team;
};

var array<ChatStruct> ChatMessages;
var config string ChatMenuClass;
var transient GUIPage ChatMenu;
var bool bTeamChatOnly;

// RO
//var transient UT2MusicManager MusicManager;				// Obsolete
var config string StatsPromptMenuClass;		// Menu that appears when connecting to a stats enabled server
var config string MusicManagerClassName;
var config string WaitingGameClassName;
var config string NeedPasswordMenuClass;	// Menu that appears when connecting to a passworded server
var config string ServerInfoMenu;			// Menu that appears when press F2

// _RO_
var config string SteamLoginMenuClass;		// Menu that appears when you need to relogin to Steam
var int SteamLoginRetryCount;				// Number of times the client has tried to autoreconnect when they get a SteamAuthStalled error
// end _RO_

delegate OnChat(string Msg, int TeamIndex);

function OnStatsClosed(optional bool bCancelled)
{
	if ( bCancelled )
		return;

	OnStatsConfigured();
}

function OnStatsConfigured()
{
//	DelayedConsoleCommand("reconnect");
	ViewportOwner.GUIController.CloseAll(false);
	ViewportOwner.Actor.ClientTravel(LastURL,TRAVEL_Absolute,false);
}

event ConnectFailure(string FailCode,string URL)
{
	local string			Error, Server;
	local int				i,Index;

	LastURL = URL;
	Server = Left(URL,InStr(URL,"/"));

	i = instr(FailCode," ");
	if (i>0)
	{
		Error = Right(FailCode,len(FailCode)-i-1);
		FailCode = Left(FailCode,i);
	}

	log("Connect Failure: "@FailCode$"["$Error$"] ("$URL$")");

	if(FailCode == "NEEDPW")
	{

		for(Index = 0;Index < SavedPasswords.Length;Index++)
		{
			if(SavedPasswords[Index].Server == Server)
			{
			    ViewportOwner.Actor.ClearProgressMessages();
				ViewportOwner.Actor.ClientTravel(URL$"?password="$SavedPasswords[Index].Password,TRAVEL_Absolute,false);
				return;
			}
		}

		LastConnectedServer = Server;
		if ( ViewportOwner.GUIController.OpenMenu(NeedPasswordMenuClass, URL, FailCode) )
			return;
	}
	else if(FailCode == "WRONGPW")
	{
		ViewportOwner.Actor.ClearProgressMessages();

		for(Index = 0;Index < SavedPasswords.Length;Index++)
		{
			if(SavedPasswords[Index].Server == Server)
			{
				SavedPasswords.Remove(Index,1);
				SaveConfig();
			}
		}

		LastConnectedServer = Server;
		if ( ViewportOwner.GUIController.OpenMenu(NeedPasswordMenuClass, URL, FailCode) )
			return;
	}
	else if(FailCode == "NEEDSTATS")
	{
		ViewportOwner.Actor.ClearProgressMessages();
		if ( ViewportOwner.GUIController.OpenMenu(StatsPromptMenuClass, "", FailCode) )
		{
			GUIController(ViewportOwner.GUIController).ActivePage.OnReopen = OnStatsConfigured;
			return;
		}
	}
	else if ( FailCode == "LOCALBAN" )
	{
		ViewportOwner.Actor.ClearProgressMessages();
		ViewportOwner.GUIController.OpenMenu(class'GameEngine'.default.DisconnectMenuClass,Localize("Errors","ConnectionFailed","Engine"),class'AccessControl'.default.IPBanned);
		return;
	}

	else if ( FailCode == "SESSIONBAN" )
	{
		ViewportOwner.Actor.ClearProgressMessages();
		ViewportOwner.GUIController.OpenMenu(class'GameEngine'.default.DisconnectMenuClass,Localize("Errors","ConnectionFailed","Engine"),class'AccessControl'.default.SessionBanned);
		return;
	}

	else if ( FailCode == "SERVERFULL" )
	{
		ViewportOwner.Actor.ClearProgressMessages();
		ViewportOwner.GUIController.OpenMenu(class'GameEngine'.default.DisconnectMenuClass,ServerFullMsg);
		return;
	}

	else if ( FailCode == "CHALLENGE" )
	{
		ViewportOwner.Actor.ClearProgressMessages();
		ViewportOwner.Actor.ClientNetworkMessage("FC_Challege","");
		return;
	}
	// _RO_
	else if ( FailCode == "STEAMLOGGEDINELSEWHERE" )
	{
		ViewportOwner.Actor.ClearProgressMessages();

		LastConnectedServer = Server;

		if ( ViewportOwner.GUIController.OpenMenu(SteamLoginMenuClass, URL, FailCode) )
			return;
	}
	else if ( FailCode == "STEAMVACBANNED" )
	{
		ViewportOwner.Actor.ClearProgressMessages();
		ViewportOwner.Actor.ClientNetworkMessage("ST_VACBan","");
		return;
	}
	else if ( FailCode == "STEAMVALIDATIONSTALLED")
	{
		// Lame hack for a Steam problem. Take this out when Valve fixes the SteamValidationStalled bug
		if( SteamLoginRetryCount < 5 )
		{
			SteamLoginRetryCount++;

			ViewportOwner.Actor.ClientTravel( URL,TRAVEL_Absolute,false);
			ViewportOwner.GUIController.CloseAll(false,True);
			return;
		}
		else
		{
			ViewportOwner.Actor.ClearProgressMessages();
			ViewportOwner.Actor.ClientNetworkMessage("ST_Unknown","");
			return;
		}
	}
	else if ( FailCode == "STEAMAUTH" /*|| FailCode == "STEAMVALIDATIONSTALLED"*/)
	{
		ViewportOwner.Actor.ClearProgressMessages();
		ViewportOwner.Actor.ClientNetworkMessage("ST_Unknown","");
		return;
	}
	// end _RO_

	log("Unhandled connection failure!  FailCode '"$FailCode@"'   URL '"$URL$"'");
	ViewportOwner.Actor.ProgressCommand("menu:"$class'GameEngine'.default.DisconnectMenuClass,FailCode,Error);
}


event NotifyLevelChange()
{
	Super.NotifyLevelChange();
	if ( VoiceChannels.Length > 0 )
		VoiceChannels.Remove(0, VoiceChannels.Length);

}


////// End Speech Menu

exec function CLS()
{
	SBHead = 0;
	ScrollBack.Remove(0,ScrollBack.Length);
}

function PostRender( canvas Canvas );	// Subclassed in state

function Chat(coerce string Msg, float MsgLife, PlayerReplicationInfo PRI)
{
	local int index;

	Message(Msg,MsgLife);		// For compatibility

    Index = ChatMessages.Length;
	ChatMessages.Length = Index+1;
    ChatMessages[Index].Message = Msg;

    if ( (PRI != None) && (PRI.Team!=None) )
	    ChatMessages[Index].Team = PRI.Team.TeamIndex;
    else
    	ChatMessages[Index].Team = 2;

	if ( (!bTeamChatOnly) || (PRI == None) || (PRI.Team==None) || (PRI.Team == ViewportOwner.Actor.PlayerReplicationInfo.Team) )
	{
		OnChat(Msg, ChatMessages[Index].team);
		OnChatMessage(Msg);
	}

	if (ChatMessages.Length>100)
    	ChatMessages.Remove(0,1);
}

delegate OnChatMessage(string Msg);

event Message( coerce string Msg, float MsgLife)
{
	if (ScrollBack.Length==MaxScrollBackSize)	// if full, Remove Entry 0
	{
		ScrollBack.Remove(0,1);
		SBHead = MaxScrollBackSize-1;
	}
	else
		SBHead++;

	ScrollBack.Length = ScrollBack.Length + 1;

	Scrollback[SBHead] = Msg;
	Super.Message(Msg,MsgLife);
}

event bool KeyEvent( EInputKey Key, EInputAction Action, FLOAT Delta )
{
	if (Key==ConsoleHotKey)
	{
		if(Action==IST_Release)
			ConsoleOpen();
		return true;
	}

    return Super.KeyEvent(Key,Action,Delta);
}


function PlayConsoleSound(Sound S)
{
	if(ViewportOwner == None || ViewportOwner.Actor == None || ViewportOwner.Actor.Pawn == None)
		return;

	ViewportOwner.Actor.ClientPlaySound(S);//,true,ConsoleSoundVol);
}

//-----------------------------------------------------------------------------
// State used while typing a command on the console.

event NativeConsoleOpen()
{
	ConsoleOpen();
}

exec function ConsoleOpen()
{
	UnPressButtons();
	TypedStr = "";
	TypedStrPos = 0;
	bCtrl = False;
	bAlt = False;
	bShift = False;

	GotoState('ConsoleVisible');
	PlayConsoleSound(SMOpenSound);
}

exec function ConsoleClose()
{
	TypedStr="";
	TypedStrPos = 0;

	bCtrl = False;
	bAlt = False;
	bShift = False;

    if( GetStateName() == 'ConsoleVisible' )
	{
		PlayConsoleSound(SMDenySound);
        GotoState( '' );
	}
}

exec function ConsoleToggle()
{
	log("console toggle");
	UnPressButtons();

    if( GetStateName() == 'ConsoleVisible' )
        ConsoleClose();
    else
        ConsoleOpen();
}

state ConsoleVisible
{
	function bool KeyType( EInputKey Key, optional string Unicode )
	{
		local PlayerController PC;

		if (bIgnoreKeys || bConsoleHotKey)
			return true;

		if (ViewportOwner != none)
			PC = ViewportOwner.Actor;

		if (bCtrl && PC != none)
		{
			if (Key == 3) //copy
			{
				PC.CopyToClipboard(TypedStr);
				return true;
			}
			else if (Key == 22) //paste
			{
				TypedStr $= PC.PasteFromClipboard();
				TypedStrPos += Len(PC.PasteFromClipboard());
				return true;
			}
			else if (Key == 24) // cut
			{
				PC.CopyToClipboard(TypedStr);
				TypedStr="";
				TypedStrPos = 0;
				return true;
			}
		}

		if( Key>=0x20 )
		{
			if( Unicode != "" )
				TypedStr = Left(TypedStr, TypedStrPos) $ Unicode $ Right(TypedStr, Len(TypedStr) - TypedStrPos);
			else
				TypedStr = Left(TypedStr, TypedStrPos) $ Chr(Key) $ Right(TypedStr, Len(TypedStr) - TypedStrPos);
			TypedStrPos++;
            return( true );
		}

		return( true );
	}

	function bool KeyEvent( EInputKey Key, EInputAction Action, FLOAT Delta )
	{
		local string Temp;
		local int i;

		if ( Action == IST_Release )
		{
			if ( Key == ConsoleHotKey )
			{
				if ( bConsoleHotKey ) ConsoleClose();
				return True;
			}

			switch ( Key )
			{
			case IK_Ctrl:
				bCtrl = False;
				break;

			case IK_Alt:
				bAlt = False;
				break;

			case IK_Shift:
				bShift = False;
				break;

			case IK_Escape:
				if ( TypedStr != "" )
				{
					TypedStr = "";
					TypedStrPos = 0;
					HistoryCur = HistoryTop;
				}

				else ConsoleClose();
				return True;

			default:
				return True;
			}
		}

		else if ( Action == IST_Press )
		{
			bIgnoreKeys = False;

			if ( Key == ConsoleHotKey )
			{
				bConsoleHotKey = True;
				return True;
			}

			switch ( Key )
			{
			case IK_Ctrl:
				bCtrl = True;
				break;

			case IK_Alt:
				bAlt = True;
				break;

			case IK_Shift:
				bShift = True;
				break;

			case IK_Escape:
				return True;

			case IK_Enter:
				if( TypedStr!="" )
				{
					// Print to console.

					History[HistoryTop] = TypedStr;
	                HistoryTop = (HistoryTop+1) % ArrayCount(History);

					if ( ( HistoryBot == -1) || ( HistoryBot == HistoryTop ) )
	                    HistoryBot = (HistoryBot+1) % ArrayCount(History);

					HistoryCur = HistoryTop;

					// Make a local copy of the string.
					Temp=TypedStr;
					TypedStr="";
					TypedStrPos=0;

					if( !ConsoleCommand( Temp ) )
						Message( Localize("Errors","Exec","Core"), 6.0 );

					Message( "", 6.0 );
				}

	            return true;

	        case IK_Up:
				if ( HistoryBot >= 0 )
				{
					if (HistoryCur == HistoryBot)
						HistoryCur = HistoryTop;
					else
					{
						HistoryCur--;
						if (HistoryCur<0)
	                        HistoryCur = ArrayCount(History)-1;
					}

					TypedStr = History[HistoryCur];
					TypedStrPos = Len(TypedStr);
				}
	            return true;

	        case IK_Down:
				if ( HistoryBot >= 0 )
				{
					if (HistoryCur == HistoryTop)
						HistoryCur = HistoryBot;
					else
	                    HistoryCur = (HistoryCur+1) % ArrayCount(History);

					TypedStr = History[HistoryCur];
					TypedStrPos = Len(TypedStr);
				}

				return True;

			case IK_Backspace:
				if( TypedStrPos > 0 )
				{
					TypedStr = Left(TypedStr,TypedStrPos-1)$Right(TypedStr, Len(TypedStr) - TypedStrPos);
					TypedStrPos--;
				}

           		return true;

           	case IK_Delete:
				if ( TypedStrPos < Len(TypedStr) )
					TypedStr = Left(TypedStr,TypedStrPos)$Right(TypedStr, Len(TypedStr) - TypedStrPos - 1);
				return true;

			case IK_Left:
				i = TypedStrPos - 1;

				if ( bCtrl )
					while ( i > 0 && Mid(TypedStrPos,i,1) != " " )
						i--;

				TypedStrPos = Max(0, i);
				return true;

			case IK_Right:
				i = TypedStrPos + 1;

				if ( bCtrl )
					while ( i <= Len(TypedStr) && Mid(TypedStr,i,1) != " " )
						i++;


				TypedStrPos = Min(Len(TypedStr), i);
				return true;

			case IK_Home:
				TypedStrPos = 0;
				return true;

			case IK_End:
				TypedStrPos = Len(TypedStr);
				return true;

			case IK_PageUp:
			case IK_MouseWheelUp:
				if (SBPos<ScrollBack.Length-1)
				{
					if (bCtrl)
						SBPos+=5;
					else
						SBPos++;

					if (SBPos>=ScrollBack.Length)
					  SBPos = ScrollBack.Length-1;
				}

				return true;

			case IK_PageDown:
			case IK_MouseWheelDown:
				if (SBPos>0)
				{
					if (bCtrl)
						SBPos-=5;
					else
						SBPos--;

					if (SBPos<0)
						SBPos = 0;
				}
			}
		}

		return True;
	}

    function BeginState()
	{
		SBPos = 0;
        bVisible= true;
		bIgnoreKeys = true;
		bConsoleHotKey = false;
        HistoryCur = HistoryTop;
		bCtrl = false;
    }
    function EndState()
    {
        bVisible = false;
		bCtrl = false;
		bConsoleHotKey = false;
    }

	function PostRender( canvas Canvas )
	{

		local float fw,fh;
		local float yclip,y;
		local int idx;

		Canvas.Font = class'HudBase'.static.GetConsoleFont(Canvas);
		yclip = canvas.ClipY*0.5;
		Canvas.StrLen("X",fw,fh);

		Canvas.SetPos(-5,-5);
		Canvas.SetDrawColor(255,255,255,255);
		Canvas.Style=1;
// if _RO_
        Canvas.DrawTileStretched(Texture'KF_InterfaceArt_tex.Menu.thin_border_SlightTransparent',Canvas.ClipX + 10,yClip + 7);
// else
//		Canvas.DrawTileStretched(material'ConsoleBack',Canvas.ClipX,yClip);
// end if _RO_
		Canvas.Style=1;

		Canvas.SetPos(0,yclip-1);
		Canvas.SetDrawColor(255,255,255,255);
// if _RO_
        //Canvas.DrawTile(Texture'InterfaceArt_tex.Menu.RODisplay',Canvas.ClipX,2,0,0,64,2);
// else
//		Canvas.DrawTile(texture 'InterfaceContent.Menu.BorderBoxA',Canvas.ClipX,2,0,0,64,2);
// end if _RO_

		Canvas.SetDrawColor(255,255,255,255);

		Canvas.SetPos(0,yclip-5-fh);
		Canvas.DrawTextClipped("(>"@Left(TypedStr, TypedStrPos)$chr(4)$Eval( TypedStrPos < Len(TypedStr), Mid(TypedStr, TypedStrPos), "_"), true);

		idx = SBHead - SBPos;
		y = yClip-y-5-(fh*2);

		if (ScrollBack.Length==0)
			return;

		Canvas.SetDrawColor(255,255,255,255);
		while (y>fh && idx>=0)
		{
			Canvas.SetPos(0,y);
			Canvas.DrawText(Scrollback[idx],false);
			idx--;
			y-=fh;
		}
	}
}

//---------------------------------------------------------------------------------------------------------------//
//---------------------------------------------------------------------------------------------------------------//
//---------------------------------------------------------------------------------------------------------------//

delegate OnExecAddFavorite( ServerFavorite Fav );
exec function AddCurrentToFavorites()
{
	local string address, ipString, portString;
	local int colonPos, portNum;
	local ServerFavorite NewFav;

	if( ViewportOwner == None || ViewportOwner.Actor == None )
		return;

	// Get current network address
	address = ViewportOwner.Actor.GetServerNetworkAddress();

	if(address == "")
		return;

	// Parse text to find IP and possibly port number
	colonPos = InStr(address, ":");
	if(colonPos < 0)
	{
		// No colon - assume port 7777
		ipString = address;
		portNum = 7777;
	}
	else
	{	// Parse out port number
		ipString = Left(address, colonPos);
		portString = Mid(address, colonPos+1);
		portNum = int(portString);
	}

	NewFav.IP = ipString;
	NewFav.Port = portNum;
	NewFav.QueryPort = portNum + 1;

	if ( AddFavorite( NewFav ) )
		ViewportOwner.Actor.ClientMessage(AddedCurrentHead@address@AddedCurrentTail);

	OnExecAddFavorite( NewFav );
}

static function bool InFavorites( ServerFavorite Fav )
{
	local int i;

	if ( Fav.IP == "" )
		return false;

	if ( Fav.Port == 0 )
		Fav.Port = 7777;

	if ( Fav.QueryPort == 0 )
		Fav.QueryPort = Fav.Port + 1;

	for ( i = 0; i < default.Favorites.Length; i++ )
	{
		if ( Fav.IP == default.Favorites[i].IP &&
		     Fav.Port == default.Favorites[i].Port &&
		     Fav.QueryPort == default.Favorites[i].QueryPort )
	    return true;
	}

	return false;
}

// Returns true only if this was a new server
static function bool AddFavorite( ServerFavorite NewFav )
{
	local int i;
	local bool bNew;

	if ( NewFav.IP == "" )
		return false;

	if ( NewFav.Port == 0 )
		NewFav.Port = 7777;

	if ( NewFav.QueryPort == 0 )
		NewFav.QueryPort = NewFav.Port + 1;

	bNew = True;
	for ( i = 0; i < default.Favorites.Length; i++ )
	{
		if ( NewFav.IP == default.Favorites[i].IP &&
		     NewFav.Port == default.Favorites[i].Port &&
		     NewFav.QueryPort == default.Favorites[i].QueryPort )
		{
			if ( NewFav.ServerName ~= default.Favorites[i].ServerName )
				return false;

			bNew = False;
			break;
		}
	}

	default.Favorites[i] = NewFav;
	StaticSaveConfig();
	return bNew;
}

static function bool RemoveFavorite( string IP, int Port, int QueryPort )
{
	local int i;

	for ( i = 0; i < default.Favorites.Length; i++ )
	{
		if ( default.Favorites[i].IP == IP && default.Favorites[i].Port == Port && default.Favorites[i].QueryPort == QueryPort )
		{
			default.Favorites.Remove(i,1);
			StaticSaveConfig();
			return true;
		}
	}

	return false;
}

static function GetFavorites( out array<ServerFavorite> List )
{
	List = default.Favorites;
}

static function SaveFavorites()
{
	// TODO: Add bDirty tracking, to only save if favorites have actually been modified

	StaticSaveConfig();
}

//---------------------------------------------------------------------------------------------------------------//
//---------------------------------------------------------------------------------------------------------------//
//---------------------------------------------------------------------------------------------------------------//

exec function SpeechMenuToggle()
{
	// Don't show speech menu if no voice pack type
    if( ViewportOwner.Actor.PlayerReplicationInfo.bOnlySpectator || (ViewportOwner.Actor.PlayerReplicationInfo.VoiceType == None) )
		return;

	GotoState('SpeechMenuVisible');
}

state SpeechMenuVisible
{
	exec function SpeechMenuToggle()
	{
		GotoState('');
	}

	function bool KeyType( EInputKey Key, optional string Unicode )
	{
		if (bIgnoreKeys)
			return true;

		return false;
	}

	function class<TeamVoicePack> GetVoiceClass()
	{
		if(ViewportOwner == None || ViewportOwner.Actor == None || ViewportOwner.Actor.PlayerReplicationInfo == None)
			return None;

		return class<TeamVoicePack>(ViewportOwner.Actor.PlayerReplicationInfo.VoiceType);
	}

	// JTODO: Bubble sort. Sorry. But I already wrote the GUIList sort today and its late.
	function SortSMArray()
	{
		local int i,j, tmpInt;
		local string tmpString;

		for(i=0; i<SMArraySize-1; i++)
		{
			for(j=i+1; j<SMArraySize; j++)
			{
				if(SMNameArray[i] > SMNameArray[j])
				{
					tmpString = SMNameArray[i];
					SMNameArray[i] = SMNameArray[j];
					SMNameArray[j] = tmpString;

					tmpInt = SMIndexArray[i];
					SMIndexArray[i] = SMIndexArray[j];
					SMIndexArray[j] = tmpInt;
				}
			}
		}
	}

	// Rebuild the array of options based on the state we are now in.
	function RebuildSMArray()
	{
		local int i, index;
		local class<TeamVoicePack> tvp;
		local GameReplicationInfo GRI;
		local PlayerReplicationInfo MyPRI;
		local VoiceChatReplicationInfo VRI;
		local UnrealPlayer up;
		local name GameMesgGroup;
		local Pawn TauntPawn;
		local bool bShowJoin, bShowLeave, bShowTalk;

		SMArraySize = 0;
		SMOffset=0;

		tvp = GetVoiceClass();
		if(tvp == None)
			return;

		//Log("TVP:"$tvp$" NumTaunts:"$tvp.Default.numTaunts);
		switch (SMState)
		{
		case SMS_Main:
			if ( VoiceChatAllowed() )
			{
				SMNameArray[SMArraySize] = SMStateName[1];
				SMIndexArray[SMArraySize] = 1;
				SMArraySize++;
			}

			if ( ViewportOwner.Actor.PlayerReplicationInfo != None && !ViewportOwner.Actor.PlayerReplicationInfo.bOnlySpectator )
			{
				for(i=2; i<7; i++)
				{
					SMNameArray[SMArraySize] = SMStateName[i];
					SMIndexArray[SMArraySize] = i;
					SMArraySize++;
				}
				if ( (ViewportOwner.Actor.Pawn != None) && !ViewportOwner.Actor.Pawn.IsA('RedeemerWarhead') )
				{
					SMNameArray[SMArraySize] = SMStateName[7];
					SMIndexArray[SMArraySize] = 7;
					SMArraySize++;
				}
			}

			if ( SMArraySize == 0 )
				GotoState('');

			break;

		case SMS_Taunt:
			for(i=0; i<tvp.Default.numTaunts; i++)
			{
				if(tvp.Default.MatureTaunt[i] == 1 && ViewportOwner.Actor.bNoMatureLanguage)
					continue;

				if(tvp.Default.TauntAbbrev[i] != "")
					SMNameArray[SMArraySize] = tvp.Default.TauntAbbrev[i];
				else
					SMNameArray[SMArraySize] = tvp.Default.TauntString[i];

				SMIndexArray[SMArraySize] = i;
				SMArraySize++;
			}

			SortSMArray();
			break;

		case SMS_Ack:
			for(i=0; i<tvp.Default.numAcks; i++)
			{

            	if (tvp.Default.AckAbbrev[i] != "")
					SMNameArray[SMArraySize] = tvp.Default.AckAbbrev[i];
                else
					SMNameArray[SMArraySize] = tvp.Default.AckString[i];
				SMIndexArray[SMArraySize] = i;
				SMArraySize++;
			}

			SortSMArray();
			break;

		case SMS_FriendFire:
			for(i=0; i<tvp.Default.numFFires; i++)
			{
				if(tvp.Default.FFireAbbrev[i] != "")
					SMNameArray[SMArraySize] = tvp.Default.FFireAbbrev[i];
				else
					SMNameArray[SMArraySize] = tvp.Default.FFireString[i];

				SMIndexArray[SMArraySize] = i;
				SMArraySize++;
			}

			SortSMArray();
			break;

		case SMS_Order:
			for(i=0; i<9; i++)
			{
				if(tvp.Default.OrderSound[i] == None)
					continue;

				index = tvp.static.OrderToIndex(i, ViewportOwner.Actor.Level.GetGameClass());

				if(tvp.Default.OrderAbbrev[index] != "")
					SMNameArray[SMArraySize] = tvp.Default.OrderAbbrev[index];
				else
					SMNameArray[SMArraySize] = tvp.Default.OrderString[index];

				SMIndexArray[SMArraySize] = index;
				SMArraySize++;
			}

			break;

		case SMS_Other:
			GameMesgGroup = ViewportOwner.Actor.Level.GetGameClass().Default.OtherMesgGroup;

			for(i=0; i<ArrayCount(tvp.Default.OtherString); i++)
			{
				if(tvp.Default.OtherSound[i] == None)
					continue;

				// If we have defined a group for this message, only put it in the menu if it matches the current game group.
				if(tvp.Default.OtherMesgGroup[i] != ''  && tvp.Default.OtherMesgGroup[i] != GameMesgGroup)
						continue;

				if(tvp.Default.OtherAbbrev[i] != "")
					SMNameArray[SMArraySize] = tvp.Default.OtherAbbrev[i];
				else
					SMNameArray[SMArraySize] = tvp.Default.OtherString[i];

				SMIndexArray[SMArraySize] = i;
				SMArraySize++;
			}
			break;

		case SMS_PlayerSelect:
			if(ViewportOwner == None || ViewportOwner.Actor == None || ViewportOwner.Actor.PlayerReplicationInfo == None)
				return;

			GRI = ViewportOwner.Actor.GameReplicationInfo;
			MyPRI = ViewportOwner.Actor.PlayerReplicationInfo;

			// First entry is to send to 'all'
			// HACK: Don't let you send 'Hold This Position' to all all bots
			if( SMIndex != 1)
			{
				SMNameArray[SMArraySize] = SMAllString;
				SMArraySize++;
			}

			for(i=0; i<GRI.PRIArray.Length; i++)
			{
				if ( GRI.bTeamGame )
				{
					// Dont put player on list if myself, not on a team, on the same team, or a spectator.
					if( GRI.PRIArray[i].Team == None || MyPRI.Team == None )
						continue;

					if( GRI.PRIArray[i].Team.TeamIndex != MyPRI.Team.TeamIndex )
						continue;
				}

				if( GRI.PRIArray[i].TeamId == MyPRI.TeamId )
					continue;

				if( GRI.PRIArray[i].bOnlySpectator )
					continue;

				SMNameArray[SMArraySize] = GRI.PRIArray[i].PlayerName;
				SMArraySize++;
				// Dont need a number- we use the name direct
			}

			break;

		case SMS_TauntAnim:
			if(ViewportOwner == None || ViewportOwner.Actor == None)
				return;

			up = UnrealPlayer(ViewportOwner.Actor);
			if(up == None || up.Pawn == None)
				return;

			if ( Vehicle(up.Pawn) != None )
				TauntPawn = Vehicle(up.Pawn).Driver;
			else
				TauntPawn = up.Pawn;

			for(i=0; i<TauntPawn.TauntAnims.Length; i++)
			{
				SMNameArray[SMArraySize] = TauntPawn.TauntAnimNames[Clamp(i,0,15)];  // clamped because taunt array is max 8, see Pawn.uc
				SMIndexArray[SMArraySize] = i;
				SMArraySize++;
			}

			SortSMArray();
			break;

		case SMS_VoiceChat:
			if(	ViewportOwner == None || ViewportOwner.Actor == None ||
				ViewportOwner.Actor.PlayerReplicationInfo == None || ViewportOwner.Actor.VoiceReplicationInfo == None)
			{
				log("VoiceChatChannel not displaying.  ViewportOwner:"$ViewportOwner@"Actor:"$ViewportOwner.Actor@"MyPRI:"$ViewportOwner.Actor.PlayerReplicationInfo@"VRI:"$ViewportOwner.Actor.VoiceReplicationInfo,'VoiceChat');
				return;
			}

			VRI = ViewportOwner.Actor.VoiceReplicationInfo;
			MyPRI = ViewportOwner.Actor.PlayerReplicationInfo;

			VoiceChannels = VRI.GetChannels();
			for ( i = 0; i < VoiceChannels.Length; i++ )
			{
				if ( VoiceChannels[i].CanJoinChannel(MyPRI) )
				{
					bShowTalk = True;
					if ( VoiceChannels[i].IsMember(MyPRI, True) )
						bShowLeave = True;
					else bShowJoin = True;
				}
			}

			// Only display talk option if there are any channels that we can talk in.
			if ( bShowTalk )
			{
				// Store the currently selected channel name
				SMNameArray[SMArraySize] = SMChannelOptions[2]; // Talk
				SMIndexArray[SMArraySize] = 2;
				SMArraySize++;
			}

			// Only display join option if there are any channels that we can join
			if ( bShowJoin )
			{
				SMNameArray[SMArraySize] = SMChannelOptions[0];	// Join
				SMIndexArray[SMArraySize] = 0;
				SMArraySize++;
			}

			// Only display leave option if we are a member of any channels
			if ( bShowLeave )
			{
				SMNameArray[SMArraySize] = SMChannelOptions[1];	// Leave
				SMIndexArray[SMArraySize] = 1;
				SMArraySize++;
			}

			break;

		case SMS_VoiceChatChannel:
			if(	ViewportOwner == None || ViewportOwner.Actor == None ||
				ViewportOwner.Actor.VoiceReplicationInfo == None)
			{
				log(Name@"No VoiceReplicationInfo so not generating VoiceChat menu",'VoiceChat');
				return;
			}

			VRI = ViewportOwner.Actor.VoiceReplicationInfo;
			MyPRI = ViewportOwner.Actor.PlayerReplicationInfo;
			SMStateName[ESpeechMenuState.EnumCount - 1] = SMChannelOptions[SMIndex];

			switch ( SMIndex )
			{
			case 0:    // Join
				VoiceChannels = VRI.GetChannels();
				for ( i = 0; i < VoiceChannels.Length; i++ )
				{
					if ( VoiceChannels[i].CanJoinChannel(MyPRI) && !VoiceChannels[i].IsMember(MyPRI, True) )
					{
						SMNameArray[SMArraySize] = VoiceChannels[i].GetTitle();
						SMIndexArray[SMArraySize] = VoiceChannels[i].ChannelIndex;
						SMArraySize++;
					}
				}
				break;

			case 1:    // Leave
				VoiceChannels = VRI.GetChannels();
				for ( i = 0; i < VoiceChannels.Length; i++ )
				{
					if ( VoiceChannels[i].CanJoinChannel(MyPRI) && VoiceChannels[i].IsMember(MyPRI, True) )
					{
						SMNameArray[SMArraySize] = VoiceChannels[i].GetTitle();
						SMIndexArray[SMArraySize] = VoiceChannels[i].ChannelIndex;
						SMArraySize++;
					}
				}
				break;

			case 2:    // Talk
				VoiceChannels = VRI.GetChannels();
				for ( i = 0; i < VoiceChannels.Length; i++ )
				{
					if ( VoiceChannels[i].CanJoinChannel(MyPRI) )
					{
						SMNameArray[SMArraySize] = VoiceChannels[i].GetTitle();
						SMIndexArray[SMArraySize] = VoiceChannels[i].ChannelIndex;
						SMArraySize++;
					}
				}

				break;

			}
			break;
		}
	}

	//////////////////////////////////////////////

	function EnterState(ESpeechMenuState newState, optional bool bNoSound)
	{
		SMState = newState;
		HighlightRow = 0;
		RebuildSMArray();

		if(!bNoSound)
			PlayConsoleSound(SMAcceptSound);
	}

	function LeaveState() // Go up a level
	{
		PlayConsoleSound(SMDenySound);

		switch ( SMState )
		{
			case SMS_Main:
				CloseSpeechMenu();
				break;

			case SMS_PlayerSelect:
				EnterState(SMS_Order, true);
				break;

			case SMS_VoiceChatChannel:
				EnterState(SMS_VoiceChat,True);
				break;

			default:
				EnterState(SMS_Main, True);
		}
	}
	// // // // // //

	function HandleInput(int keyIn)
	{
		local int selectIndex;
		local UnrealPlayer up;
		local Pawn TauntPawn;
		local VoiceChatReplicationInfo VRI;

		// GO BACK - previous state (might back out of menu);
		if(keyIn == -1)
		{
			HighlightRow = 0;
			LeaveState();
			return;
		}

		// TOP LEVEL - we just enter a new state
		if(SMState == SMS_Main)
		{
			switch( SMNameArray[keyIn-1] )
			{
			case SMStateName[1]: SMType = ''; if ( VoiceChatAllowed() ) EnterState( SMS_VoiceChat ); break;
			case SMStateName[2]: SMType = 'ACK'; EnterState(SMS_Ack); break;
			case SMStateName[3]: SMType = 'FRIENDLYFIRE'; EnterState(SMS_FriendFire); break;
			case SMStateName[4]: SMType = 'ORDER'; EnterState(SMS_Order); break;
			case SMStateName[5]: SMType = 'OTHER'; EnterState(SMS_Other); break;
			case SMStateName[6]: SMType = 'TAUNT'; EnterState(SMS_Taunt); break;
			case SMStateName[7]: SMType = ''; EnterState(SMS_TauntAnim); break;
			}

			return;
		}

		// Next page on the same level
		if(keyIn == 0 )
		{
			// Check there is a next page!
			if(SMArraySize - SMOffset > 9)
				SMOffset += 9;

			return;
		}

		// Previous page on the same level
		if(keyIn == -2)
		{
			SMOffset = Max(SMOffset - 9, 0);
			return;
		}

		// Otherwise - we have selected something!
		selectIndex = SMOffset + keyIn - 1;
		if(selectIndex < 0 || selectIndex >= SMArraySize) // discard - out of range selections.
			return;

		switch ( SMState )
		{
		case SMS_Order:
			SMIndex = SMIndexArray[selectIndex];
			EnterState(SMS_PlayerSelect);
			break;

		case SMS_VoiceChat:
			SMIndex = SMIndexArray[selectIndex];
			EnterState(SMS_VoiceChatChannel);
			break;

		case SMS_VoiceChatChannel:
			VRI = ViewportOwner.Actor.VoiceReplicationInfo;
			if (VRI == None)
				return;

			// Perform the action selected
			switch ( SMIndex )
			{
				case 0:	// Join Channel
					ViewportOwner.Actor.Join(SMNameArray[selectIndex],"");
					break;

				case 1:	// Leave Channel
					ViewportOwner.Actor.Leave(SMNameArray[selectIndex]);
					break;

				case 2:
					ViewportOwner.Actor.Speak(SMNameArray[selectIndex]);
					break;
			}

			// Add confirmation
			PlayConsoleSound(SMAcceptSound);
			CloseSpeechMenu();
			break;

		case SMS_PlayerSelect:
			if(SMNameArray[selectIndex] == SMAllString)
				ViewportOwner.Actor.Speech(SMType, SMIndex, "");
			else
				ViewportOwner.Actor.Speech(SMType, SMIndex, SMNameArray[selectIndex]);

			PlayConsoleSound(SMAcceptSound);

			CloseSpeechMenu(); // Close menu after message
			break;

		case SMS_TauntAnim:
			up = UnrealPlayer(ViewportOwner.Actor);
			if ( Vehicle(up.Pawn) != None )
				TauntPawn = Vehicle(up.Pawn).Driver;
			else
				TauntPawn = up.Pawn;
			up.Taunt( TauntPawn.TauntAnims[ SMIndexArray[selectIndex] ] );
			PlayConsoleSound(SMAcceptSound);
			CloseSpeechMenu();
			break;

		default:
			ViewportOwner.Actor.Speech(SMType, SMIndexArray[selectIndex], "");
			PlayConsoleSound(SMAcceptSound);
			CloseSpeechMenu();
		}
	}

	//////////////////////////////////////////////

	function string NumberToString(int num)
	{
		local EInputKey key;
		local string s;

		if(num < 0 || num > 9)
			return "";

		if(bSpeechMenuUseLetters)
			key = LetterKeys[num];
		else
			key = NumberKeys[num];

		s = ViewportOwner.Actor.ConsoleCommand( "LOCALIZEDKEYNAME"@string(int(key)) );
		return s;
	}

	function DrawNumbers( canvas Canvas, int NumNums, bool IncZero, bool sizing, out float XMax, out float YMax )
	{
		local int i;
		local float XPos, YPos;
		local float XL, YL;

		XPos = Canvas.ClipX * (SMOriginX+SMMargin);
		YPos = Canvas.ClipY * (SMOriginY+SMMargin);
		Canvas.SetDrawColor(128,255,128,255);

		for(i=0; i<NumNums; i++)
		{
			Canvas.SetPos(XPos, YPos);
			if(!sizing)
				Canvas.DrawText(NumberToString(i+1)$"-", false);
			else
			{
				Canvas.TextSize(NumberToString(i+1)$"-", XL, YL);
				XMax = Max(XMax, XPos + XL);
				YMax = Max(YMax, YPos + YL);
			}

			YPos += SMLineSpace;
		}

		if(IncZero)
		{
			Canvas.SetPos(XPos, YPos);

			if(!sizing)
				Canvas.DrawText(NumberToString(0)$"-", false);

			XPos += SMTab;
			Canvas.SetPos(XPos, YPos);

			if(!sizing)
				Canvas.DrawText(SMMoreString, false);
			else
			{
				Canvas.TextSize(SMMoreString, XL, YL);
				XMax = Max(XMax, XPos + XL);
				YMax = Max(YMax, YPos + YL);
			}
		}
	}

	function DrawCurrentArray( canvas Canvas, bool sizing, out float XMax, out float YMax )
	{
		local int i, stopAt;
		local float XPos, YPos;
		local float XL, YL;

		XPos = (Canvas.ClipX * (SMOriginX+SMMargin)) + SMTab;
		YPos = Canvas.ClipY * (SMOriginY+SMMargin);

		stopAt = Min(SMOffset+9, SMArraySize);
		for(i=SMOffset; i<stopAt; i++)
		{
			Canvas.SetPos(XPos, YPos);
			if(!sizing)
			{
				if ( SMState == SMS_VoiceChatChannel )
				{
					if ( IsActiveChannel(SMOffset + i) )
						Canvas.SetDrawColor(0,255,0,255);
					else if ( SMIndex == 2 && !IsMember(SMOffset + i) )
						Canvas.SetDrawColor(160,160,160,255);
					else Canvas.SetDrawColor(255,255,255,255);
				}
				else Canvas.SetDrawColor(255,255,255,255);

				Canvas.DrawText(SMNameArray[i], false);
			}
			else
			{
				Canvas.TextSize(SMNameArray[i], XL, YL);
				XMax = Max(XMax, XPos + XL);
				YMax = Max(YMax, YPos + YL);
			}

			YPos += SMLineSpace;
		}
	}

	function bool IsActiveChannel(int i)
	{
		if ( SMState != SMS_VoiceChatChannel )
			return false;

		if ( ViewportOwner.Actor == None || ViewportOwner.Actor.ActiveRoom == None )
			return false;

		if ( i < 0 || i > SMArraySize )
			return false;

		if ( SMIndexArray[i] != ViewportOwner.Actor.ActiveRoom.ChannelIndex )
			return false;

		return true;
	}

	function bool IsMember(int i)
	{
		if ( SMState != SMS_VoiceChatChannel )
			return false;

		if ( ViewportOwner.Actor == None || ViewportOwner.Actor.VoiceReplicationInfo == None || ViewportOwner.Actor.PlayerReplicationInfo == None )
			return false;

		return ViewportOwner.Actor.VoiceReplicationInfo.IsMember(ViewportOwner.Actor.PlayerReplicationInfo, SMIndexArray[i]);
	}



	//////////////////////////////////////////////

	function int KeyToNumber(EInputKey InKey)
	{
		local int i;

		for(i=0; i<10; i++)
		{
			if(bSpeechMenuUseLetters)
			{
				if(InKey == LetterKeys[i])
					return i;
			}
			else
			{
				if(InKey == NumberKeys[i])
					return i;
			}
		}

		return -1;
	}

	function bool KeyEvent( EInputKey Key, EInputAction Action, FLOAT Delta )
	{
		local int input, NumNums;

		NumNums = Min(SMArraySize - SMOffset, 10);

		// While speech menu is up, dont let user use console. Debateable.
		//if( KeyIsBoundTo( Key, "ConsoleToggle" ) )
		//	return true;
		//if( KeyIsBoundTo( Key, "Type" ) )
		//	return true;

		if (Action == IST_Press)
		{
			bIgnoreKeys=false;
			if ( Key == IK_Ctrl )
				bCtrl = True;

			else if ( Key == IK_Alt )
				bAlt = True;

			else if ( key == IK_Shift )
				bShift = True;
		}

		if ( Action == IST_Release )
		{
			if ( bAlt )
			{
				if ( Key == IK_Left )	// extra mouse buttons - only tested this on logitech mx500  :\
				{
					HandleInput(-1);
					return True;
				}

				else if ( Key == IK_Right )
				{
					input = HighlightRow + 1;
					if(input == 10)
						input = 0;

					HighlightRow=0;
					HandleInput(input);
					return True;
				}
			}
		}

		if( Action != IST_Press )
			return false;

		if( Key==IK_Escape)
		{
			HandleInput(-1);
			return true ;
		}

		// If 'letters' mode is on, convert input
		input = KeyToNumber(Key);
		if(input != -1)
		{
			HandleInput(input);
			return true;
		}

		// Keys below are only used if bSpeechMenuUseMouseWheel is true
		if(!bSpeechMenuUseMouseWheel)
			return false;

		if( Key==IK_MouseWheelUp )
		{
			// If moving up on the top row, and there is a previous page
			if(HighlightRow == 0 && SMOffset > 0)
			{
				HandleInput(-2);
				HighlightRow=9;
			}
			else
			{
				HighlightRow = Max(HighlightRow - 1, 0);
			}

			return true;
		}
		else if( Key==IK_MouseWheelDown )
		{
			// If moving down on the bottom row (the 'MORE' row), act as if we hit it, and move highlight to top.
			if(HighlightRow == 9)
			{
				HandleInput(0);
				HighlightRow=0;
			}
			else
			{
				HighlightRow = Min(HighlightRow + 1, NumNums - 1);
			}

			return true;
		}
		else if( Key==IK_MiddleMouse )
		{

			input = HighlightRow + 1;
			if(input == 10)
				input = 0;

			HighlightRow=0;
			HandleInput(input);
			return true;
		}

		return false;
	}

	function Font MyGetSmallFontFor(canvas Canvas)
	{
		local int i;
		for(i=1; i<8; i++)
		{
			if ( class'HudBase'.default.FontScreenWidthSmall[i] <= Canvas.ClipX )
				return class'HudBase'.static.LoadFontStatic(i-1);
		}
		return class'HudBase'.static.LoadFontStatic(7);
	}

	function PostRender( canvas Canvas )
	{
		local float XL, YL;
		local int SelLeft, i;
		local float XMax, YMax;

		Canvas.Font = class'UT2MidGameFont'.static.GetMidGameFont(Canvas.ClipX); // Update which font to use.

		// Figure out max key name size
		XMax = 0;
		YMax = 0;
		for(i=0; i<10; i++)
		{
			Canvas.TextSize(NumberToString(i)$"- ", XL, YL);
			XMax = Max(XMax, XL);
			YMax = Max(YMax, YL);
		}
		SMLineSpace = YMax * 1.1;
		SMTab = XMax;

		SelLeft = SMArraySize - SMOffset;

		// First we figure out how big the bounding box needs to be
		XMax = 0;
		YMax = 0;
		DrawNumbers( canvas, Min(SelLeft, 9), SelLeft > 9, true, XMax, YMax);
		DrawCurrentArray( canvas, true, XMax, YMax);
		Canvas.TextSize(SMStateName[SMState], XL, YL);
		XMax = Max(XMax, Canvas.ClipX*(SMOriginX+SMMargin) + XL);
		YMax = Max(YMax, (Canvas.ClipY*SMOriginY) - (1.2*SMLineSpace) + YL);
		// XMax, YMax now contain to maximum bottom-right corner we drew to.

		// Then draw the box
		XMax -= Canvas.ClipX * SMOriginX;
		YMax -= Canvas.ClipY * SMOriginY;
		Canvas.SetDrawColor(255,255,255,255);
		Canvas.SetPos(Canvas.ClipX * SMOriginX, Canvas.ClipY * SMOriginY);
// if _RO_
// else
//		Canvas.DrawTileStretched(texture 'InterfaceContent.Menu.BorderBoxD', XMax + (SMMargin*Canvas.ClipX), YMax + (SMMargin*Canvas.ClipY));
// end if _RO_

		// Draw highlight
		if(bSpeechMenuUseMouseWheel)
		{
			Canvas.SetDrawColor(255,255,255,128);
			Canvas.SetPos( Canvas.ClipX*SMOriginX, Canvas.ClipY*(SMOriginY+SMMargin) + ((HighlightRow - 0.1)*SMLineSpace) );
// if _RO_
// else
//			Canvas.DrawTileStretched(texture 'InterfaceContent.Menu.BorderBoxD', XMax + (SMMargin*Canvas.ClipX), 1.1*SMLineSpace );
// end if _RO_
		}

		// Then actually draw the stuff
		DrawNumbers( canvas, Min(SelLeft, 9), SelLeft > 9, false, XMax, YMax);
		DrawCurrentArray( canvas, false, XMax, YMax);

		// Finally, draw a nice title bar.
		Canvas.SetDrawColor(255,255,255,255);
		Canvas.SetPos(Canvas.ClipX*SMOriginX, (Canvas.ClipY*SMOriginY) - (1.5*SMLineSpace));
// if _RO_
// else
//		Canvas.DrawTileStretched(texture 'InterfaceContent.Menu.BorderBoxD', XMax + (SMMargin*Canvas.ClipX), (1.5*SMLineSpace));
// end if _RO_

		Canvas.SetDrawColor(255,255,128,255);
		Canvas.SetPos(Canvas.ClipX*(SMOriginX+SMMargin), (Canvas.ClipY*SMOriginY) - (1.2*SMLineSpace));
		Canvas.DrawText(SMStateName[SMState]);

		if (SMState == SMS_VoiceChatChannel)
			DrawMembers(Canvas, XMax, YMax);
	}

	function DrawMembers(Canvas Canvas, float XMax, float YMax)
	{
		local array<int> Members;
		local int i;
		local float XPos, YPos, XL, YL;
		local GameReplicationInfo GRI;
		local string CurrentPlayer;

		GRI = ViewportOwner.Actor.GameReplicationInfo;
		if ( GRI == None )
			return;

		if (HighlightRow >= 0 && HighlightRow < SMArraySize)
		{
			Members = ViewportOwner.Actor.VoiceReplicationInfo.GetChannelMembersAt( SMIndexArray[SMOffset + HighlightRow] );
			Canvas.SetDrawColor(255,255,175,220);
	//		XPos = (Canvas.ClipX * (SMOriginX+SMMargin)) + SMTab;
	//		YPos = Canvas.ClipY * (SMOriginY+SMMargin);
			XPos = XMax + (SMMargin * Canvas.ClipX * 2.25)/* + SMTab*/;
			YPos = Canvas.ClipY*(SMOriginY+SMMargin) + ((HighlightRow + 0.1)*SMLineSpace);

			for(i=0; i<Members.Length; i++)
			{
				CurrentPlayer = GRI.FindPlayerByID(Members[i]).PlayerName;
				Canvas.SetPos(XPos, YPos);
				Canvas.TextSize(CurrentPlayer, XL, YL);
				XMax = Max(XMax, XPos + XL);
				YMax = Max(YMax, YPos + YL);

				YPos += SMLineSpace;
				Canvas.DrawText( CurrentPlayer );
			}
		}
	}

    function BeginState()
	{
        bVisible = true;
		bIgnoreKeys = true;
		bCtrl = false;
		HighlightRow=0;

		EnterState(SMS_Main, true);
		SMCallsign="";

		PlayConsoleSound(SMOpenSound);
	}

	function CloseSpeechMenu()
	{
		if (!bSpeechMenuLocked)
			GoToState('');
	}

    function EndState()
    {
        bVisible = false;
		bCtrl = false;
    }

	// Close speech menu on level change
	event NotifyLevelChange()
	{
		Global.NotifyLevelChange();
		GotoState('');
	}
}

exec function InGameChat()
{
	local GUIController GC;

    GC = GUIController(ViewportOwner.GUIController);
	if ( GC.OpenMenu(ChatMenuClass) )
    	ChatMenu = GC.ActivePage;

}

exec function ServerInfo()
{
	local GUIController GC;

    GC = GUIController(ViewportOwner.GUIController);

    if (GC==None)
    	return;

	GC.OpenMenu(ServerInfoMenu);
}

exec function TeamChatOnly()
{
	bTeamChatOnly = !bTeamChatOnly;
}

exec function PlayWaitingGame()
{
	local GUIController GC;

	if (WaitingGameClassName == "")
		return;

	GC = GUIController(ViewportOwner.GUIController);
	if (GC != None)
		GC.OpenMenu(WaitingGameClassName);
}

exec function MusicMenu()
{
	local GUIController C;
	local int i;

	if ( MusicManagerClassName == "" )
	{
		log("No music player menu configured.  Please check the MusicManagerClassName line of the [XInterface.ExtendedConsole] section of the UT2004.ini.");
		return;
	}

	C = GUIController(ViewportOwner.GUIController);
	if ( C != None )
	{
		i = C.FindMenuIndexByName(MusicManagerClassName);
		if ( i == -1 )
			C.OpenMenu(MusicManagerClassName);
		else C.RemoveMenuAt(i,true);
	}
}

// For debugging PlayInfo
exec function DumpPlayInfo( string group )
{
	local PlayInfo PInfo;

	foreach AllObjects( class'PlayInfo', PInfo )
	{
		if ( PInfo.InfoClasses.Length > 0 && PInfo.Settings.Length > 0 )
		{
			PInfo.Dump(group);
			break;
		}
	}
}
// For debugging CacheRecords
exec function DumpRecords(string Type)
{
	DumpCacheRecords(Type);
}

final private function AddMessage(string Mesg)
{
	log(Mesg);
	Message(Mesg,0);
}

final function DumpCacheRecords(optional string CacheType)
{
	local int i;
	local string Margin;

    local array<CacheManager.CrosshairRecord> CRecs;
    local array<CacheManager.WeaponRecord> WRecs;
    local array<CacheManager.MapRecord> MRecs;
    local array<CacheManager.MutatorRecord> MutRecs;
    local array<CacheManager.GameRecord> GRecs;
    local array<CacheManager.AnnouncerRecord> ARecs;
    local array<CacheManager.VehicleRecord> VRecs;

	if ( CacheType == "" || CacheType ~= "Crosshair" )
	{
	    class'CacheManager'.static.GetCrosshairList(CRecs);
	    AddMessage(" ================ Cached crosshair records ================ ");
	    for (i = 0; i < CRecs.Length; i++)
	    {
	        AddMessage(CRecs[i].RecordIndex$")"@CRecs[i].FriendlyName@CRecs[i].CrosshairTexture);
	    }

	    AddMessage("");
	}

	if ( CacheType == "" || CacheType ~= "GameType" )
	{
	    class'CacheManager'.static.GetGameTypeList(GRecs);
	    AddMessage(" ================ Cached gametype records ================ ");
	    for (i = 0; i < GRecs.Length; i++)
	    {
	    	for (Margin="";Len(Margin)<Len(i);Margin$=" ");

	        AddMessage(GRecs[i].RecordIndex$")"@GRecs[i].ClassName);
	        AddMessage(Margin$"    Name        :"$GRecs[i].GameName);
	        AddMessage(Margin$"    Description :"$GRecs[i].Description);
	        AddMessage(Margin$"    TextName    :"$GRecs[i].TextName);
	        AddMessage(Margin$"    GameAcronym :"$GRecs[i].GameAcronym);
	        AddMessage(Margin$"    MapListType :"$GRecs[i].MapListClassName);
	        AddMessage(Margin$"    MapPrefix   :"$GRecs[i].MapPrefix);
	        AddMessage(Margin$"    bTeamGame   :"$GRecs[i].bTeamGame);
	        AddMessage(Margin$"    Group       :"$GRecs[i].GameTypeGroup);
	    }
	    AddMessage("");
	}

	if ( CacheType == "" || CacheType ~= "Weapon" )
	{
 	   class'CacheManager'.static.GetWeaponList(WRecs);
	    AddMessage(" ================ Cached weapon records ================ ");
	    for (i = 0; i < WRecs.Length; i++)
	    {
	    	for (Margin="";Len(Margin)<Len(i);Margin$=" ");

	        AddMessage(WRecs[i].RecordIndex$")"@WRecs[i].ClassName);
	        AddMessage(Margin$"    FriendlyName:"$WRecs[i].FriendlyName);
	        AddMessage(Margin$"    Description :"$WRecs[i].Description);
	        AddMessage(Margin$"    TextName    :"$WRecs[i].TextName);
	        AddMessage(Margin$"    PickupClass :"$WRecs[i].PickupClassName);
	        AddMessage(Margin$"    Attachment  :"$WRecs[i].AttachmentClassName);
	    }
	    AddMessage("");
	}

	if ( CacheType == "" || CacheType ~= "Map" )
	{
	    class'CacheManager'.static.GetMapList(MRecs);
	    AddMessage(" ================ Cached map records ================ ");
	    for (i = 0; i < MRecs.Length; i++)
	    {
	    	for (Margin="";Len(Margin)<Len(i);Margin$=" ");

	        AddMessage(MRecs[i].RecordIndex$")"@MRecs[i].MapName);
	        AddMessage(Margin$"    Acronym       :"$MRecs[i].Acronym);
	        AddMessage(Margin$"    TextName      :"$MRecs[i].TextName);
	        AddMessage(Margin$"    FriendlyName  :"$MRecs[i].FriendlyName);
	        AddMessage(Margin$"    Author        :"$MRecs[i].Author);
	        AddMessage(Margin$"    PlayerCountMin:"$MRecs[i].PlayerCountMin);
	        AddMessage(Margin$"    PlayerCountMax:"$MRecs[i].PlayerCountMax);
	        AddMessage(Margin$"    Description   :"$MRecs[i].Description);
	        AddMessage(Margin$"    Screenshot    :"$MRecs[i].ScreenshotRef);
	        AddMessage(Margin$"    ExtraInfo     :"$MRecs[i].ExtraInfo);
	    }

	    AddMessage("");
	}

	if ( CacheType == "" || CacheType ~= "Mutator" )
	{
	    class'CacheManager'.static.GetMutatorList(MutRecs);
	    AddMessage(" ================ Cached mutator records ================ ");
	    for (i = 0; i < MutRecs.Length; i++)
	    {
	    	for (Margin="";Len(Margin)<Len(i);Margin$=" ");

	        AddMessage(MutRecs[i].RecordIndex$")"@MutRecs[i].ClassName);
	        AddMessage(Margin$"    FriendlyName       :"$MutRecs[i].FriendlyName);
	        AddMessage(Margin$"    Description        :"$MutRecs[i].Description);
	        AddMessage(Margin$"    GroupName          :"$MutRecs[i].GroupName);
	        AddMessage(Margin$"    ConfigMenu         :"$MutRecs[i].ConfigMenuClassName);
	        AddMessage(Margin$"    IconMaterialName   :"$MutRecs[i].IconMaterialName);
	    }
	    AddMessage("");
	}

	if ( CacheType == "" || CacheType ~= "Announcer" )
	{
	    class'CacheManager'.static.GetAnnouncerList(ARecs);
	    AddMessage(" ================ Cached announcer records ================ ");
	    for (i = 0; i < ARecs.Length; i++)
	    {
	    	for (Margin="";Len(Margin)<Len(i);Margin$=" ");

	        AddMessage(ARecs[i].RecordIndex$")"@ARecs[i].ClassName);
	        AddMessage(Margin$"    FriendlyName       :"$ARecs[i].FriendlyName);
	        AddMessage(Margin$"    PackageName        :"$ARecs[i].PackageName);
	        AddMessage(Margin$"    FallbackPackage    :"$ARecs[i].FallbackPackage);
	    }
	    AddMessage("");
	}

	if ( CacheType == "" || CacheType ~= "Vehicle" )
	{
	    class'CacheManager'.static.GetVehicleList(VRecs);
	    AddMessage(" ================ Cached vehicle records ================ ");
	    for (i = 0; i < VRecs.Length; i++)
	    {
	    	for (Margin="";Len(Margin)<Len(i);Margin$=" ");

	        AddMessage(VRecs[i].RecordIndex$")"@VRecs[i].ClassName);
	        AddMessage(Margin$"    FriendlyName       :"$VRecs[i].FriendlyName);
	        AddMessage(Margin$"    Description        :"$VRecs[i].Description);
	    }
	    AddMessage("");
	}
}

function bool VoiceChatAllowed()
{
	if ( ViewportOwner == None )
		return false;

	if ( ViewportOwner.Actor == None )
		return false;

	if ( ViewportOwner.Actor.Level == None )
		return false;

	if ( ViewportOwner.Actor.Level.NetMode == NM_DedicatedServer )
		return false;

	if ( ViewportOwner.Actor.Level.NetMode == NM_StandAlone )
		return false;

	return true;
}

exec function DLO( string ClassName, string ClassType )
{
	local class c;
	local object o;

	if ( ClassName == "" )
	{
		log("No class name specified.");
		return;
	}

	if ( ClassType != "" )
		c = class(DynamicLoadObject(ClassType,Class'Class'));

	else c = class'class';

	o = DynamicLoadObject(ClassName, c);
	log("Result of DLO was "$o);
}

exec function DumpLoadingHints(string param)
{
	local array<CacheManager.GameRecord> Recs;
	local int i, j;
	local bool bShowAll;

	local class<GameInfo> GameClass;
	local array<string> Hints;

	class'CacheManager'.static.GetGameTypeList(Recs);

	bShowAll = param == "";
	for ( i = 0; i < Recs.Length; i++ )
	{
		GameClass = class<GameInfo>(DynamicLoadObject( Recs[i].ClassName, class'Class' ));
		if ( GameClass != None )
		{
			Hints = GameClass.static.GetAllLoadHints(!bShowAll);
			if ( Hints.Length > 0 )
			{
				log( Recs[i].GameName @ "Loading Hints -" );
				for ( j = 0; j < Hints.Length; j++ )
					log("  "$j$") "$Hints[j] );

				log("");
			}
		}
	}
}

exec function DebugTabOrder()
{
	if ( GUIController(ViewportOwner.GUIController) != None && GUIController(ViewportOwner.GUIController).ActivePage != None )
	{
		log("Searching for components with invalid tab order...");
		GUIController(ViewportOwner.GUIController).ActivePage.DebugTabOrder();
	}
}

defaultproperties
{
	SMOriginX=0.01
	SMOriginY=0.3
	SMMargin=0.015

	SMStateName(0)="Speech Menu"
	SMStateName(1)="Voice Chat"
	SMStateName(2)="Acknowledge"
	SMStateName(3)="Friendly Fire"
	SMStateName(4)="Order"
	SMStateName(5)="Other"
	SMStateName(6)="Taunt"
	SMStateName(7)="Taunt Anim"
	SMStateName(8)="Player Select"

	SMAllString="[ALL]"
	SMMoreString="[MORE]"

    // Replaceme
	SMOpenSound=sound'KF_MenuSnd.msfxEdit'
	SMAcceptSound=sound'KF_MenuSnd.msfxMouseClick'
	SMDenySound=sound'KF_MenuSnd.CharFade'
	ConsoleSoundVol=0.3

	MaxScrollbackSize=128
	bSpeechMenuUseMouseWheel=True
	bSpeechMenuUseLetters=False

	LetterKeys(0)=IK_Q
	LetterKeys(1)=IK_W
	LetterKeys(2)=IK_E
	LetterKeys(3)=IK_R
	LetterKeys(4)=IK_A
	LetterKeys(5)=IK_S
	LetterKeys(6)=IK_D
	LetterKeys(7)=IK_F
	LetterKeys(8)=IK_Z
	LetterKeys(9)=IK_X

	NumberKeys(0)=IK_0
	NumberKeys(1)=IK_1
	NumberKeys(2)=IK_2
	NumberKeys(3)=IK_3
	NumberKeys(4)=IK_4
	NumberKeys(5)=IK_5
	NumberKeys(6)=IK_6
	NumberKeys(7)=IK_7
	NumberKeys(8)=IK_8
	NumberKeys(9)=IK_9

	SMChannelOptions(0)="Join"
	SMChannelOptions(1)="Leave"
	SMChannelOptions(2)="Talk"

	AddedCurrentHead="Added Server:"
	AddedCurrentTail="To Favorites!"
	StatsPromptMenuClass="GUI2K4.UT2K4StatsPrompt"
	NeedPasswordMenuClass="GUI2K4.UT2K4GetPassword"
	SteamLoginMenuClass="ROInterface.ROSteamLoginPage"
	ServerInfoMenu="GUI2K4.UT2K4ServerInfo"
	ChatMenuClass="GUI2K4.UT2K4InGameChat"
	ServerFullMsg="Server is now full"
}
