// GameInfo.
//
// The GameInfo defines the game being played: the game rules, scoring, what actors
// are allowed to exist in this game type, and who may enter the game.  While the
// GameInfo class is the public interface, much of this functionality is delegated
// to several classes to allow easy modification of specific game components.  These
// classes include GameInfo, AccessControl, Mutator, BroadcastHandler, and GameRules.
// A GameInfo actor is instantiated when the level is initialized for gameplay (in
// C++ UGameEngine::LoadMap() ).  The class of this GameInfo actor is determined by
// (in order) either the DefaultGameType if specified in the LevelInfo, or the
// DefaultGame entry in the game's .ini file (in the Engine.Engine section), unless
// its a network game in which case the DefaultServerGame entry is used.
//
//=============================================================================
class GameInfo extends Info
    HideDropDown
    CacheExempt
    native;

//------------------------------------------------------------------------------
// Structs for reporting server state data

struct native export KeyValuePair
{
    var() string Key;
    var() string Value;
};

struct native export PlayerResponseLine
{
    var() int PlayerNum;
    var() string PlayerName;
    var() int Ping;
    var() int Score;
    var() int StatsID;
};

struct native export ServerResponseLine
{
    var() int ServerID;
    var() string IP;
    var() int Port;
    var() int QueryPort;
    var() string ServerName;
    var() string MapName;
    var() string GameType;
    var() int CurrentPlayers;
    var() int MaxPlayers;
    var() int CurrentWave;
    var() int FinalWave;
    var() int Ping;
    var() int Flags;
    var() string SkillLevel;
    var() array<KeyValuePair> ServerInfo;
    var() array<PlayerResponseLine> PlayerInfo;
};

//-----------------------------------------------------------------------------
// Variables.

var bool                        bRestartLevel;            // Level should be restarted when player dies
var bool                        bPauseable;               // Whether the game is pauseable.
var config bool                 bWeaponStay;              // Whether or not weapons stay when picked up.
var bool                        bCanChangeSkin;           // Allow player to change skins in game.
var cache bool                  bTeamGame;                // This is a team game.
var bool                        bGameEnded;               // set when game ends
var bool                        bOverTime;
var localized bool              bAlternateMode;
var bool                        bCanViewOthers;
var bool                        bDelayedStart;
var bool                        bWaitingToStartMatch;
var globalconfig bool           bChangeLevels;
var bool                        bAlreadyChanged;
var bool                        bLoggingGame;           // Does this gametype log?
var bool                        bEnableStatLogging;     // If True, games will log
var config bool                 bAllowWeaponThrowing;
var globalconfig bool           bAllowBehindView;
var globalconfig bool           bAdminCanPause;
var bool                        bGameRestarted;
var globalconfig bool           bWeaponShouldViewShake;
var bool                        bModViewShake;          // for mutators to turn off weaponviewshake
var bool                        bForceClassicView;      // OBSOLETE
var globalconfig bool           bLowGore;               // OBSOLETE - use GoreLevel instead;
var bool bWelcomePending;
var bool bAttractCam;
var bool    bMustJoinBeforeStart;   // players can only spectate if they join after the game starts
var bool bTestMode;
var bool bAllowVehicles;    //are vehicles allowed in this gametype?
var bool bAllowMPGameSpeed;
var bool bIsSaveGame;           // stays true during entire game (unlike LevelInfo property)
var bool bLiberalVehiclePaths;
var globalconfig bool bLargeGameVOIP;

var globalconfig    int         GoreLevel;
var globalconfig    float       GameDifficulty;
var float       AutoAim;                  // OBSOLETE How much autoaiming to do (1 = none, 0 = always).
                                                        // (cosine of max error to correct)
var globalconfig    float       GameSpeed;                // Scale applied to game rate.
var   float                     StartTime;

var   string                    DefaultPlayerClassName;

// user interface
var   string                  ScoreBoardType;           // Type of class<Menu> to use for scoreboards. (gam)
var   string                  BotMenuType;              // Type of bot menu to display. -NOT USED-
var   cache string            RulesMenuType;            // Type of rules menu to display.
var   string                  SettingsMenuType;         // Type of settings menu to display. -NOT USED-
var   string                  GameUMenuType;            // Type of Game dropdown to display. (Used as map menu in Onslaught)
var   string                  MultiplayerUMenuType;     // Type of Multiplayer dropdown to display. -NOT USED-
var   string                  GameOptionsMenuType;      // Type of options dropdown to display. -NOT USED-

var   cache string            HUDSettingsMenu;          // Optional GUI page for configuring HUD options specific to this gametype
var   string                  HUDType;                  // HUD class this game uses.
var   cache string            MapListType;              // Maplist this game uses.
var   cache string            MapPrefix;                // Prefix characters for names of maps for this game type.
var   string                  BeaconName;               // Identifying string used for finding LAN servers.
var localized string          GoreLevelText[3];
var() int                     ResetCountDown;
var() config int              ResetTimeDelay;           // time (seconds) before restarting teams

var   globalconfig int        MaxSpectators;            // Maximum number of spectators.
var   int                     NumSpectators;            // Current number of spectators.
var   globalconfig int        MaxPlayers;
var   int                     NumPlayers;               // number of human players
var   int                     NumBots;                  // number of non-human players (AI controlled but participating as a player)
var   int                     CurrentID;
var localized string          DefaultPlayerName;
var float                     FearCostFallOff;          // how fast the FearCost in NavigationPoints falls off

var config int                GoalScore;                // what score is needed to end the match
var config int                MaxLives;                 // max number of lives for match, unless overruled by level's GameDetails
var config int                TimeLimit;                // time limit in minutes

// Message classes.
var class<LocalMessage>       DeathMessageClass;
var class<GameMessage>        GameMessageClass;
var name                      OtherMesgGroup;

//-------------------------------------
// GameInfo components
var   string                  MutatorClass;
var   Mutator                 BaseMutator;            // linked list of Mutators (for modifying actors as they enter the game)
var() globalconfig string     AccessControlClass;
var   AccessControl           AccessControl;          // AccessControl controls whether players can enter and/or become admins
var   GameRules               GameRulesModifiers;     // linked list of modifier classes which affect game rules
var() string                  BroadcastHandlerClass;
var() class<BroadcastHandler> BroadcastClass;
var   BroadcastHandler        BroadcastHandler;       // handles message (text and localized) broadcasts

var class<PlayerController>   PlayerControllerClass;  // type of player controller to spawn for players logging in
var string                    PlayerControllerClassName;

// ReplicationInfo
var() class<GameReplicationInfo> GameReplicationInfoClass;
var GameReplicationInfo          GameReplicationInfo;

// Voice chat
var() class<VoiceChatReplicationInfo>   VoiceReplicationInfoClass;
var VoiceChatReplicationInfo            VoiceReplicationInfo;

// Maplist management
var   globalconfig string         MaplistHandlerType;
var   class<MaplistManagerBase>   MaplistHandlerClass;
var   transient MaplistManagerBase          MaplistHandler;

// Stats - jmw
var GameStats                   GameStats;              // Holds the GameStats actor
var globalconfig string         GameStatsClass;         // Type of GameStats actor to spawn

// Server demo recording - rjp
var transient string            DemoCommand;    // set in InitGame to value of ?DemoRec=
                                                // only checked in GameInfo.PostBeginPlay()


// Cheat Protection
var globalconfig string         SecurityClass;

// Cache-related information
var() cache localized string    GameName;
var() cache localized string    Description;
var() cache string              ScreenShotName;
var() cache String              DecoTextName;   // deprecated ?
var() cache String              Acronym;

// voting handler
var globalconfig string VotingHandlerType;
var class<VotingHandler> VotingHandlerClass;
var transient VotingHandler VotingHandler;

// persistant game data management
var() transient GameProfile     CurrentGameProfile;
var() private const transient Manifest    SaveGameManifest;

// localized PlayInfo descriptions & extra info
const GIPROPNUM = 15;
var localized string GIPropsDisplayText[GIPROPNUM];
var localized string GIPropDescText[GIPROPNUM];
var localized string GIPropsExtras[2];

var Vehicle VehicleList;
var string CallSigns[15];

var globalconfig string ServerSkillLevel;   // The Server Skill Level ( 0 - 2, Beginner - Advanced )

var globalconfig float MaxIdleTime;     // maximum time players are allowed to idle before being kicked

//Loading screen hints
var localized string NoBindString;
var color BindColor;

// if _RO_
var   globalconfig bool        bIgnore32PlayerLimit;  // This will allow the game to run with more than 32 players. WARNING - Will most likely overload the server, only use with monster machines

var   globalconfig bool        bVACSecured;            // Attempt to launch the server in VAC secured mode.
// end _RO_

// if _KF_
var bool    bCustomGameLength;  // Whether or not the Game Length was set to Custom at the start of this game
var class<InternetInfo> TcpLinkClass;
var InternetInfo        TcpLink;
// end _KF_

native final function Manifest  GetSavedGames();
native final function Object    CreateDataObject ( class objClass, string objName, string packageName );
native final function bool      DeleteDataObject ( class objClass, string objName, string packageName );
native final function Object    LoadDataObject   ( class objClass, string objName, string packageName );
native final iterator function  AllDataObjects   ( class objClass, out Object obj, string packageName );
native final function bool      SavePackage      ( string packageName );
native final function bool      DeletePackage    ( string packageName );

native final static function    LoadMapList(string MapPrefix, out array<string> Maps);

// if _RO_
native final function bool      IsVACSecured(); // Returns true if this game server is running in VAC secured mode
native final function           UpdateSteamUserData();
native final function           InitializeSteamStatsAndAchievements();

native final function string    GetServerAddress();
native final function string    GetServerRequest();
native final function bool      ServerFailedToRespond();
native final function           ServerResponded(string Response);
// end _RO_

//------------------------------------------------------------------------------
// Engine notifications.

function PreBeginPlay()
{
    StartTime = 0;
    GameReplicationInfo = Spawn(GameReplicationInfoClass);
    Level.GRI = GameReplicationInfo;

    InitGameReplicationInfo();
    InitVoiceReplicationInfo();

    // Create stat logging actor.
    InitLogging();
}

function Destroyed()
{
    CurrentGameProfile = None;
    Super.Destroyed();
}

function UpdatePrecacheMaterials()
{
    PrecacheGameTextures(Level);
}

function UpdatePrecacheStaticMeshes()
{
    PrecacheGameStaticMeshes(Level);
}

static function PrecacheGameTextures(LevelInfo myLevel);
static function PrecacheGameStaticMeshes(LevelInfo myLevel);
static function PrecacheGameAnnouncements(AnnouncerVoice V, bool bRewardSounds);

function string FindPlayerByID( int PlayerID )
{
    local PlayerReplicationInfo PRI;

    PRI = GameReplicationInfo.FindPlayerByID(PlayerID);
    if ( PRI != None )
        return PRI.PlayerName;
    return "";
}

function ChangeMap(int ContextID) // sjs
{
    local MapList MyList;
    local class<MapList> ML;
    local string MapString;

    ML = class<MapList>(DynamicLoadObject(MapListType, class'Class'));
    MyList = spawn(ML);
    MapString = MyList.GetMap(ContextID);
    MyList.Destroy();

    if( MapString == "" )
        return;

    Level.ServerTravel(MapString, false);
}
static function bool UseLowGore()
{
    return ( Default.bAlternateMode || Default.bLowGore || (Default.GoreLevel < 2) );
}

static function bool NoBlood()
{
    return ( Default.GoreLevel == 0 );
}

function PostBeginPlay()
{
    if ( MaxIdleTime > 0 )
        MaxIdleTime = FMax(MaxIdleTime, 30);

    if (GameStats!=None)
    {
        GameStats.NewGame();
        GameStats.ServerInfo();
    }

    // Do not start any demo recordings until everything has been initialized
    if ( DemoCommand != "" )
        ConsoleCommand("demorec"@DemoCommand, True);

//ifdef _RO_
    InitializeSteamStatsAndAchievements();

    TcpLink = Spawn(TcpLinkClass, self);

    if ( TcpLink != none )
    {
        TcpLink.OnServerResponded = OnServerResponded;
        TcpLink.OnServerConnectTimeout = OnServerFailedToRespond;
        TcpLink.Init(GetServerAddress(), GetServerRequest());
    }
//endif _RO_
}

//ifdef _RO_
function bool OnServerFailedToRespond()
{
    if ( !ServerFailedToRespond() )
    {
        TcpLink.Init(GetServerAddress(), GetServerRequest());
        return false;
    }

    return true;
}

function OnServerResponded(string Response)
{
    ServerResponded(Response);

    TcpLink.Destroy();
    TcpLink = none;
}
//endif _RO_

/* Reset()
reset actor to initial state - used when restarting level without reloading.
*/
function Reset()
{
    Super.Reset();
    bGameEnded = false;
    bOverTime = false;
    bWaitingToStartMatch = true;
    InitGameReplicationInfo();
    InitVoiceReplicationInfo();
}

/* InitLogging()
Set up statistics logging
*/
function InitLogging()
{
    local class <GameStats> MyGameStatsClass;

    if ( !bEnableStatLogging || !bLoggingGame || (Level.NetMode == NM_Standalone) || (Level.NetMode == NM_ListenServer) )
        return;

    MyGameStatsClass=class<GameStats>(DynamicLoadObject(GameStatsClass,class'class'));
    if (MyGameStatsClass!=None)
    {
        GameStats = spawn(MyGameStatsClass);
        if (GameStats==None)
            log("Could to create Stats Object");
    }
    else
        log("Error loading GameStats ["$GameStatsClass$"]");
}

function Timer()
{
    local NavigationPoint N;
    local int i;

    // If we are a server, broadcast a welcome message.
    if( bWelcomePending )
    {
        bWelcomePending = false;
        if ( Level.NetMode != NM_Standalone )
        {
            for ( i=0; i<GameReplicationInfo.PRIArray.Length; i++ )
                if ( (GameReplicationInfo.PRIArray[i] != None)
                    && !GameReplicationInfo.PRIArray[i].bWelcomed )
                {
                    GameReplicationInfo.PRIArray[i].bWelcomed = true;
                    if ( !GameReplicationInfo.PRIArray[i].bOnlySpectator )
                        BroadcastLocalizedMessage(GameMessageClass, 1, GameReplicationInfo.PRIArray[i]);
                    else
                        BroadcastLocalizedMessage(GameMessageClass, 16, GameReplicationInfo.PRIArray[i]);
                }
        }
    }

    BroadcastHandler.UpdateSentText();
    for ( N=Level.NavigationPointList; N!=None; N=N.NextNavigationPoint )
        N.FearCost *= FearCostFallOff;
}

// Called when game shutsdown.
event GameEnding()
{
    EndLogging("serverquit");
}

/* KickIdler() called if
        if ( (Pawn != None) || (PlayerReplicationInfo.bOnlySpectator && (ViewTarget != self))
            || (Level.Pauser != None) || Level.Game.bWaitingToStartMatch || Level.Game.bGameEnded )
        {
            LastActiveTime = Level.TimeSeconds;
        }
        else if ( (Level.Game.MaxIdleTime > 0) && (Level.TimeSeconds - LastActiveTime > Level.Game.MaxIdleTime) )
            KickIdler(self);
*/
event KickIdler(PlayerController PC)
{
    log("Kicking idle player "$PC.PlayerReplicationInfo.PlayerName);
    AccessControl.DefaultKickReason = AccessControl.IdleKickReason;
    AccessControl.KickPlayer(PC);
    AccessControl.DefaultKickReason = AccessControl.Default.DefaultKickReason;
}

//------------------------------------------------------------------------------
// Replication

function InitGameReplicationInfo()
{
    GameReplicationInfo.bTeamGame = bTeamGame;
    GameReplicationInfo.GameName = GameName;
    GameReplicationInfo.GameClass = string(Class);
    GameReplicationInfo.MaxLives = MaxLives;
}

function InitVoiceReplicationInfo()
{
    log(Name@"VoiceReplicationInfo created:"@VoiceReplicationInfo,'VoiceChat');
}

function InitMaplistHandler();

native function string GetNetworkNumber();

//------------------------------------------------------------------------------
// Server/Game Querying.

function GetServerInfo( out ServerResponseLine ServerState )
{
    local int RealSkillLevel;

    ServerState.ServerName      = StripColor(GameReplicationInfo.ServerName);
    ServerState.MapName         = Left(string(Level), InStr(string(Level), "."));
    ServerState.GameType        = Mid( string(Class), InStr(string(Class), ".")+1);
    ServerState.CurrentPlayers  = GetNumPlayers();
    ServerState.MaxPlayers      = MaxPlayers;
    ServerState.CurrentWave     = GetCurrentWaveNum();
    ServerState.FinalWave       = GetFinalWaveNum();
    ServerState.IP              = ""; // filled in at the other end.
    ServerState.Port            = GetServerPort();

    // rjp --
    // Valid values are 0, 1, 2
    // empty string will NOT be considered as 0 in the database, so we need to do some fiddling here
    // to make sure that servers that do not set a default value for ServerSkillLevel still report a skill level
    RealSkillLevel = Clamp( int(ServerSkillLevel), 0, 2 );
    ServerState.SkillLevel      = string(RealSkillLevel);

    ServerState.ServerInfo.Length = 0;
    ServerState.PlayerInfo.Length = 0;
}

function int GetNumPlayers()
{
    return NumPlayers;
}

function int GetCurrentWaveNum()
{
    return 0;
}

function int GetFinalWaveNum()
{
    return 0;
}

function GetServerDetails( out ServerResponseLine ServerState )
{
    local Mutator M;
    local GameRules G;
    local int i, Len, NumMutators;
    local string MutatorName;
    local bool bFound;

    AddServerDetail( ServerState, "ServerMode", Eval(Level.NetMode == NM_ListenServer, "non-dedicated", "dedicated") );
    AddServerDetail( ServerState, "AdminName", GameReplicationInfo.AdminName );
    AddServerDetail( ServerState, "AdminEmail", GameReplicationInfo.AdminEmail );
// if _RO_
    //log("Is the server vac secured - "$IsVACSecured());

    AddServerDetail( ServerState, "ServerVersion", Level.ROVersion );
    AddServerDetail( ServerState, "IsVacSecured", Eval(IsVACSecured(), "true", "false"));   //TODO write some function to grab this from the native code
// else
//  AddServerDetail( ServerState, "ServerVersion", Level.EngineVersion );

    if ( AccessControl != None && AccessControl.RequiresPassword() )
        AddServerDetail( ServerState, "GamePassword", "True" );

// if _RO_
    // no stats in RO!
// else
//  AddServerDetail( ServerState, "GameStats", GameStats != None );
// end if _RO_

    if ( AllowGameSpeedChange() && (GameSpeed != 1.0) )
        AddServerDetail( ServerState, "GameSpeed", int(GameSpeed*100)/100.0 );

    AddServerDetail( ServerState, "MaxSpectators", MaxSpectators );

    // voting
    if( VotingHandler != None )
        VotingHandler.GetServerDetails(ServerState);

    // Ask the mutators if they have anything to add.
    for (M = BaseMutator; M != None; M = M.NextMutator)
    {
        M.GetServerDetails(ServerState);
        NumMutators++;
    }

    // Ask the gamerules if they have anything to add.
    for ( G=GameRulesModifiers; G!=None; G=G.NextGameRules )
        G.GetServerDetails(ServerState);

    // make sure all the mutators were really added
    for ( i=0; i<ServerState.ServerInfo.Length; i++ )
        if ( ServerState.ServerInfo[i].Key ~= "Mutator" )
            NumMutators--;

    if ( NumMutators > 1 )
    {
        // something is missing
        for (M = BaseMutator.NextMutator; M != None; M = M.NextMutator)
        {
            MutatorName = M.GetHumanReadableName();
            for ( i=0; i<ServerState.ServerInfo.Length; i++ )
                if ( (ServerState.ServerInfo[i].Value ~= MutatorName) && (ServerState.ServerInfo[i].Key ~= "Mutator") )
                {
                    bFound = true;
                    break;
                }
            if ( !bFound )
            {
                Len = ServerState.ServerInfo.Length;
                ServerState.ServerInfo.Length = Len+1;
                ServerState.ServerInfo[i].Key = "Mutator";
                ServerState.ServerInfo[i].Value = MutatorName;
            }
        }
    }
}

function GetServerPlayers( out ServerResponseLine ServerState )
{
    local Mutator M;
    local Controller C;
    local PlayerReplicationInfo PRI;
    local int i, TeamFlag[2];

    i = ServerState.PlayerInfo.Length;
    TeamFlag[0] = 1 << 29;
    TeamFlag[1] = TeamFlag[0] << 1;

    for( C=Level.ControllerList;C!=None;C=C.NextController )
        {
            PRI = C.PlayerReplicationInfo;
            if( (PRI != None) && !PRI.bBot && MessagingSpectator(C) == None )
            {
            ServerState.PlayerInfo.Length = i+1;
            ServerState.PlayerInfo[i].PlayerNum  = C.PlayerNum;
            ServerState.PlayerInfo[i].PlayerName = PRI.PlayerName;
            ServerState.PlayerInfo[i].Score      = PRI.Score;
            ServerState.PlayerInfo[i].Ping       = 4 * PRI.Ping;
            if (bTeamGame && PRI.Team != None)
                ServerState.PlayerInfo[i].StatsID = ServerState.PlayerInfo[i].StatsID | TeamFlag[PRI.Team.TeamIndex];
            i++;
            }
    }

    // Ask the mutators if they have anything to add.
    for (M = BaseMutator.NextMutator; M != None; M = M.NextMutator)
        M.GetServerPlayers(ServerState);
}

//------------------------------------------------------------------------------
// Misc.

// Return the server's port number.
function int GetServerPort()
{
    local string S;
    local int i;

    // Figure out the server's port.
    S = Level.GetAddressURL();
    i = InStr( S, ":" );
    assert(i>=0);
    return int(Mid(S,i+1));
}

event bool IsPassworded()
{
    if ( AccessControl != none )
    {
        return AccessControl.RequiresPassword();
    }

    return false;
}

function bool SetPause( BOOL bPause, PlayerController P )
{
    if( bPauseable || (bAdminCanPause && (P.IsA('Admin') || P.PlayerReplicationInfo.bAdmin)) || Level.Netmode==NM_Standalone )
    {
        if( bPause )
            Level.Pauser=P.PlayerReplicationInfo;
        else
            Level.Pauser=None;
        return True;
    }
    else return False;
}

//------------------------------------------------------------------------------
// Game parameters.

function bool AllowGameSpeedChange()
{
    if ( Level.NetMode == NM_Standalone )
        return true;
    else
        return bAllowMPGameSpeed;
}

//
// Set gameplay speed.
//
function SetGameSpeed( float T )
{
    local float OldSpeed;

    if ( !AllowGameSpeedChange() )
    {
        Level.TimeDilation = 1.1;
        GameSpeed = 1.0;
        Default.GameSpeed = GameSpeed;
    }
    else
    {
        OldSpeed = GameSpeed;
        GameSpeed = FMax(T, 0.1);
        Level.TimeDilation = 1.1 * GameSpeed;
        if ( GameSpeed != OldSpeed )
        {
            Default.GameSpeed = GameSpeed;
            class'GameInfo'.static.StaticSaveConfig();
        }
    }
// if _RO_
    SetTimer(Level.TimeDilation/GameSpeed, true);
// else UT
//  SetTimer(Level.TimeDilation, true);
}

//
// Called after setting low or high detail mode.
//
event DetailChange()
{
    local actor A;
    local zoneinfo Z;

    if( Level.DetailMode == DM_Low )
    {
        foreach DynamicActors(class'Actor', A)
        {
            if( (A.bHighDetail || A.bSuperHighDetail) && !A.bGameRelevant )
                A.Destroy();
        }
    }
    else if( Level.DetailMode == DM_High )
    {
        foreach DynamicActors(class'Actor', A)
        {
            if( A.bSuperHighDetail && !A.bGameRelevant )
                A.Destroy();
        }
    }
    foreach AllActors(class'ZoneInfo', Z)
        Z.LinkToSkybox();
}

//------------------------------------------------------------------------------
// Player start functions

//
// Grab the next option from a string.
//
static function bool GrabOption( out string Options, out string Result )
{
    if( Left(Options,1)=="?" )
    {
        // Get result.
        Result = Mid(Options,1);
        if( InStr(Result,"?")>=0 )
            Result = Left( Result, InStr(Result,"?") );

        // Update options.
        Options = Mid(Options,1);
        if( InStr(Options,"?")>=0 )
            Options = Mid( Options, InStr(Options,"?") );
        else
            Options = "";

        return true;
    }
    else return false;
}

//
// Break up a key=value pair into its key and value.
//
static function GetKeyValue( string Pair, out string Key, out string Value )
{
    if( InStr(Pair,"=")>=0 )
    {
        Key   = Left(Pair,InStr(Pair,"="));
        Value = Mid(Pair,InStr(Pair,"=")+1);
    }
    else
    {
        Key   = Pair;
        Value = "";
    }
}

/* ParseOption()
 Find an option in the options string and return it.
*/
static function string ParseOption( string Options, string InKey )
{
    local string Pair, Key, Value;
    while( GrabOption( Options, Pair ) )
    {
        GetKeyValue( Pair, Key, Value );
        if( Key ~= InKey )
            return Value;
    }
    return "";
}

//
// HasOption - return true if the option is specified on the command line.
//
static function bool HasOption( string Options, string InKey )
{
    local string Pair, Key, Value;
    while( GrabOption( Options, Pair ) )
    {
        GetKeyValue( Pair, Key, Value );
        if( Key ~= InKey )
            return true;
    }
    return false;
}

/* Initialize the game.
 The GameInfo's InitGame() function is called before any other scripts (including
 PreBeginPlay() ), and is used by the GameInfo to initialize parameters and spawn
 its helper classes.
 Warning: this is called before actors' PreBeginPlay.
*/
event InitGame( string Options, out string Error )
{
    local string InOpt, LeftOpt;
    local int pos;
    local class<AccessControl> ACClass;
    local class<GameRules> GRClass;
    local bool bIsTutorial;

    InOpt = ParseOption( Options, "SaveGame");
    if (InOpt != "" && CurrentGameProfile == none)
    {
        CurrentGameProfile = LoadDataObject(class'GameProfile', "GameProfile", InOpt);
        if ( CurrentGameProfile != none )
            CurrentGameProfile.Initialize(self, InOpt);
        else
        {
            Log("SINGLEPLAYER GameInfo::InitGame failed to find GameProfile"@InOpt);
        }
        if ( !CurrentGameProfile.bInLadderGame )
        {
            CurrentGameProfile = None;
        }
    }

    if ( (CurrentGameProfile == None) && (Left(string(level),3) ~= "TUT") )
        bIsTutorial = true;

// if _RO_
    InOpt = ParseOption( Options,"Ignore32PlayerLimit");
    if ( InOpt != "" )
    {
        bIgnore32PlayerLimit = bool(InOpt);
        default.bIgnore32PlayerLimit = bIgnore32PlayerLimit;
    }

// if _KF_
//    if( bIgnore32PlayerLimit )
//    {
//        MaxPlayers = Clamp(GetIntOption( Options, "MaxPlayers", MaxPlayers ),0,64);
//        default.MaxPlayers = Clamp( default.MaxPlayers, 0, 64 );
//    }
//    else
//    {
// end _KF_
//        MaxPlayers = Clamp(GetIntOption( Options, "MaxPlayers", MaxPlayers ),0,6);
//        default.MaxPlayers = Clamp( default.MaxPlayers, 0, 6 );
//    }
// End _RO_

    MaxSpectators = Clamp(GetIntOption( Options, "MaxSpectators", MaxSpectators ),0,32);
    GameDifficulty = FMax(0,GetIntOption(Options, "Difficulty", GameDifficulty));
    if ( (CurrentGameProfile != None) || bIsTutorial )
    {
        if (CurrentGameProfile != None)
            GameDifficulty = CurrentGameProfile.Difficulty;
        SetGameSpeed(1.0);
    }
    AddMutator(MutatorClass);

    BroadcastClass = class<BroadcastHandler>(DynamicLoadObject(BroadcastHandlerClass,Class'Class'));
    default.BroadcastClass = BroadcastClass;    // rjp -- for PlayInfo
    BroadcastHandler = spawn(BroadcastClass);

    InOpt = ParseOption( Options, "AccessControl");
    if( InOpt != "" )
        ACClass = class<AccessControl>(DynamicLoadObject(InOpt, class'Class'));
    if ( ACClass == None )
    {
        ACClass = class<AccessControl>(DynamicLoadObject(AccessControlClass, class'Class'));
        if (ACClass == None)
            ACClass = class'Engine.AccessControl';
    }

    LeftOpt = ParseOption( Options, "AdminName" );
    InOpt = ParseOption( Options, "AdminPassword");
    if( LeftOpt!="" && InOpt!="" )
            ACClass.default.bDontAddDefaultAdmin = true;

    // Only spawn access control if we are a server
    if (Level.NetMode == NM_ListenServer || Level.NetMode == NM_DedicatedServer )
    {
        AccessControl = Spawn(ACClass);
        if (AccessControl != None && LeftOpt!="" && InOpt!="" )
            AccessControl.SetAdminFromURL(LeftOpt, InOpt);
    }

    SetGameSpeed(1.0);
    InOpt = ParseOption( Options, "GameRules");
    if ( InOpt != "" )
    {
        log("Game Rules"@InOpt);
        while ( InOpt != "" )
        {
            pos = InStr(InOpt,",");
            if ( pos > 0 )
            {
                LeftOpt = Left(InOpt, pos);
                InOpt = Right(InOpt, Len(InOpt) - pos - 1);
            }
            else
            {
                LeftOpt = InOpt;
                InOpt = "";
            }
            log("Add game rules "$LeftOpt);
            GRClass = class<GameRules>(DynamicLoadObject(LeftOpt, class'Class'));
            if ( GRClass != None )
            {
                if ( GameRulesModifiers == None )
                    GameRulesModifiers = Spawn(GRClass);
                else
                    GameRulesModifiers.AddGameRules(Spawn(GRClass));
            }
        }
    }

    InOpt = ParseOption( Options, "Mutator");
    if ( InOpt != "" )
    {
        log("Mutators"@InOpt);
        while ( InOpt != "" )
        {
            pos = InStr(InOpt,",");
            if ( pos > 0 )
            {
                LeftOpt = Left(InOpt, pos);
                InOpt = Right(InOpt, Len(InOpt) - pos - 1);
            }
            else
            {
                LeftOpt = InOpt;
                InOpt = "";
            }
            AddMutator(LeftOpt, true);
        }
    }
    if ( (CurrentGameProfile == None) && !bIsTutorial )
    {
        InOpt = ParseOption( Options, "GameSpeed");
        if( InOpt != "" )
        {
            log("GameSpeed"@InOpt);
            SetGameSpeed(float(InOpt));
        }
    }

    InOpt = ParseOption( Options, "GamePassword");
    if( InOpt != "" && AccessControl != None)
    {
        AccessControl.SetGamePassWord(InOpt);
        log( "GamePassword" @ InOpt );
    }

    InOpt = ParseOption( Options,"AllowThrowing");
    if ( InOpt != "" )
        bAllowWeaponThrowing = bool(InOpt);

    InOpt = ParseOption( Options,"AllowBehindview");
    if ( InOpt != "" )
        bAllowBehindview = bool(InOpt);

    InOpt = ParseOption(Options, "GameStats");
    if ( InOpt != "")
        bEnableStatLogging = bool(InOpt);

    log("GameInfo::InitGame : bEnableStatLogging"@bEnableStatLogging);

    if( HasOption(Options, "DemoRec") )
        DemoCommand = ParseOption(Options, "DemoRec");

    if ( HasOption(Options, "AttractCam") )
        bAttractCam = true;

    // Voting handler requires maplist manager to be configured
    InitMaplistHandler();
    InOpt = ParseOption( Options, "VotingHandler");
    if( InOpt != "" )
        VotingHandlerClass = class<VotingHandler>(DynamicLoadObject(InOpt, class'Class'));

    if( VotingHandlerClass == None )
    {
        if( VotingHandlerType != "" )
            VotingHandlerClass = class<VotingHandler>(DynamicLoadObject(VotingHandlerType, class'Class'));
        else
            VotingHandlerClass = class<VotingHandler>(DynamicLoadObject("Engine.VotingHandler", class'Class'));
    }
    class'Engine.GameInfo'.default.VotingHandlerClass = VotingHandlerClass;
    if( (Level.NetMode != NM_StandAlone) && (VotingHandlerClass != None) && VotingHandlerClass.Static.IsEnabled() )
    {
        VotingHandler = Spawn(VotingHandlerClass);
        if(VotingHandler == none)
            log("WARNING: Failed to spawn VotingHandler");
    }
    bTestMode = HasOption(Options, "CheckErrors");

// if _RO_
    InOpt = ParseOption( Options,"VACSecured");
    if ( InOpt != "" )
        bVACSecured = bool(InOpt);
// end _RO_

}

function AddMutator(string mutname, optional bool bUserAdded)
{
    local class<Mutator> mutClass;
    local Mutator mut;

    if ( !Static.AllowMutator(MutName) )
        return;

    mutClass = class<Mutator>(DynamicLoadObject(mutname, class'Class'));
    if (mutClass == None)
        return;

    if ( (mutClass.Default.GroupName != "") && (BaseMutator != None) )
    {
        // make sure no mutators with same groupname
        for ( mut=BaseMutator; mut!=None; mut=mut.NextMutator )
            if ( mut.GroupName == mutClass.Default.GroupName )
            {
                log("Not adding "$mutClass$" because already have a mutator in the same group - "$mut);
                return;
            }
    }

    // make sure this mutator is not added already
    for ( mut=BaseMutator; mut!=None; mut=mut.NextMutator )
        if ( mut.Class == mutClass )
        {
            log("Not adding "$mutClass$" because this mutator is already added - "$mut);
            return;
        }

    mut = Spawn(mutClass);
    // mc, beware of mut being none
    if (mut == None)
        return;

    // Meant to verify if this mutator was from Command Line parameters or added from other Actors
    mut.bUserAdded = bUserAdded;

    if (BaseMutator == None)
        BaseMutator = mut;
    else
        BaseMutator.AddMutator(mut);
}

function AddGameModifier( GameRules NewRule )
{
    if ( NewRule == None )
        return;

    if ( GameRulesModifiers != None )
        GameRulesModifiers.AddGameRules(NewRule);
    else GameRulesModifiers = NewRule;
}

//
// Return beacon text for serverbeacon.
//
event string GetBeaconText()
{
    return
        Level.ComputerName
    $   " "
    $   Left(Level.Title,24)
    $   "\\t"
    $   BeaconName
    $   "\\t"
    $   GetNumPlayers()
    $   "/"
    $   MaxPlayers;
}

/* ProcessServerTravel()
 Optional handling of ServerTravel for network games.
*/
function ProcessServerTravel( string URL, bool bItems )
{
    local playercontroller P, LocalPlayer;

    // Pass it along
    BaseMutator.ServerTraveling(URL,bItems);

    EndLogging("mapchange");

    // Notify clients we're switching level and give them time to receive.
    // We call PreClientTravel directly on any local PlayerPawns (ie listen server)
    log("ProcessServerTravel:"@URL);
    foreach DynamicActors( class'PlayerController', P )
        if( NetConnection( P.Player)!=None )
            P.ClientTravel( Eval( Instr(URL,"?") > 0, Left(URL,Instr(URL,"?")), URL), TRAVEL_Relative, bItems );
        else
        {
            LocalPlayer = P;
            P.PreClientTravel();
        }

    if ( (Level.NetMode == NM_ListenServer) && (LocalPlayer != None) )
        Level.NextURL = Level.NextURL
                     $"?Team="$LocalPlayer.GetDefaultURL("Team")
                     $"?Name="$LocalPlayer.GetDefaultURL("Name")
                     $"?Class="$LocalPlayer.GetDefaultURL("Class")
                     $"?Character="$LocalPlayer.GetDefaultURL("Character");

    // Switch immediately if not networking.
    if( Level.NetMode!=NM_DedicatedServer && Level.NetMode!=NM_ListenServer )
        Level.NextSwitchCountdown = 0.0;
}

//
// Accept or reject a player on the server.
// Fails login if you set the Error to a non-empty string.
//
event PreLogin
(
    string Options,
    string Address,
    string PlayerID,
    out string Error,
    out string FailCode
)
{
    local bool bSpectator;

    bSpectator = ( ParseOption( Options, "SpectatorOnly" ) ~= "1" );
    if (AccessControl != None)
        AccessControl.PreLogin(Options, Address, PlayerID,Error, FailCode, bSpectator);
}

function int GetIntOption( string Options, string ParseString, int CurrentValue)
{
    local string InOpt;

    InOpt = ParseOption( Options, ParseString );
    if ( InOpt != "" )
        return int(InOpt);
    return CurrentValue;
}

function bool BecomeSpectator(PlayerController P)
{
// if _RO_
    if ( (P.PlayerReplicationInfo == None)
         || (NumSpectators >= MaxSpectators) || P.IsInState('GameEnded') || P.IsInState('RoundEnded') )
// else
//  if ( (P.PlayerReplicationInfo == None) || !GameReplicationInfo.bMatchHasBegun
//       || (NumSpectators >= MaxSpectators) || P.IsInState('GameEnded') || P.IsInState('RoundEnded') )
// end if _RO_
    {
        P.ReceiveLocalizedMessage(GameMessageClass, 12);
        return false;
    }

    P.PlayerReplicationInfo.bOnlySpectator = true;
    NumSpectators++;
    NumPlayers--;

    return true;
}

function bool AllowBecomeActivePlayer(PlayerController P)
{
    if ( (P.PlayerReplicationInfo == None) || !GameReplicationInfo.bMatchHasBegun || bMustJoinBeforeStart
         || (NumPlayers >= MaxPlayers) || (MaxLives > 0) || P.IsInState('GameEnded') || P.IsInState('RoundEnded') )
    {
        P.ReceiveLocalizedMessage(GameMessageClass, 13);
        return false;
    }
    return true;
}

function bool AtCapacity(bool bSpectator)
{
    if ( Level.NetMode == NM_Standalone )
        return false;

    if ( bSpectator )
        return ( (NumSpectators >= MaxSpectators)
            && ((Level.NetMode != NM_ListenServer) || (NumPlayers > 0)) );
    else
        return ( (MaxPlayers>0) && (NumPlayers>=MaxPlayers) );
}

function InitSavedLevel()
{
    if ( Level.GRI == None )
        Level.GRI = GameReplicationInfo;

    if ( Level.ObjectPool == None )
        Level.ObjectPool = new(xLevel) class'ObjectPool';
}

//
// Log a player in.
// Fails login if you set the Error string.
// PreLogin is called before Login, but significant game time may pass before
// Login is called, especially if content is downloaded.
//
event PlayerController Login
(
    string Portal,
    string Options,
    out string Error
)
{
    local NavigationPoint StartSpot;
    local PlayerController NewPlayer, TestPlayer;
    local string          InName, InAdminName, InPassword, InChecksum, InCharacter,InSex;
    local byte            InTeam;
    local bool bSpectator, bAdmin;
    local class<Security> MySecurityClass;
    local pawn TestPawn;

    Options = StripColor(Options);  // Strip out color Codes

    BaseMutator.ModifyLogin(Portal, Options);

    // Get URL options.
    InName     = Left(ParseOption ( Options, "Name"), 20);
    InTeam     = GetIntOption( Options, "Team", 255 ); // default to "no team"
    InAdminName= ParseOption ( Options, "AdminName");
    InPassword = ParseOption ( Options, "Password" );
    InChecksum = ParseOption ( Options, "Checksum" );

    if ( HasOption(Options, "Load") )
    {
        log("Loading Savegame");

        InitSavedLevel();
        bIsSaveGame = true;

        // Try to match up to existing unoccupied player in level,
        // for savegames - also needed coop level switching.
        ForEach DynamicActors(class'PlayerController', TestPlayer )
        {
            if ( (TestPlayer.Player==None) && (TestPlayer.PlayerOwnerName~=InName) )
            {
                TestPawn = TestPlayer.Pawn;
                if ( TestPawn != None )
                    TestPawn.SetRotation(TestPawn.Controller.Rotation);
log("FOUND "$TestPlayer@TestPlayer.PlayerReplicationInfo.PlayerName);
                return TestPlayer;
            }
        }
    }

    bSpectator = ( ParseOption( Options, "SpectatorOnly" ) ~= "1" );
    if (AccessControl != None)
        bAdmin = AccessControl.CheckOptionsAdmin(Options);

    // Make sure there is capacity except for admins. (This might have changed since the PreLogin call).
    if ( !bAdmin && AtCapacity(bSpectator) )
    {
        Error = GameMessageClass.Default.MaxedOutMessage;
        return None;
    }

    // If admin, force spectate mode if the server already full of reg. players
    if ( bAdmin && AtCapacity(false))
        bSpectator = true;

    // Pick a team (if need teams)
    InTeam = PickTeam(InTeam,None);

    // Find a start spot.
    StartSpot = FindPlayerStart( None, InTeam, Portal );

    if( StartSpot == None )
    {
        Error = GameMessageClass.Default.FailedPlaceMessage;
        return None;
    }

    if ( PlayerControllerClass == None )
        PlayerControllerClass = class<PlayerController>(DynamicLoadObject(PlayerControllerClassName, class'Class'));

    NewPlayer = spawn(PlayerControllerClass,,,StartSpot.Location,StartSpot.Rotation);

    // Handle spawn failure.
    if( NewPlayer == None )
    {
        log("Couldn't spawn player controller of class "$PlayerControllerClass);
        Error = GameMessageClass.Default.FailedSpawnMessage;
        return None;
    }

    NewPlayer.StartSpot = StartSpot;

    // Init player's replication info
    NewPlayer.GameReplicationInfo = GameReplicationInfo;

    // Apply security to this controller
    MySecurityClass=class<Security>(DynamicLoadObject(SecurityClass,class'class'));
    if (MySecurityClass!=None)
    {
        NewPlayer.PlayerSecurity = spawn(MySecurityClass,NewPlayer);
        if (NewPlayer.PlayerSecurity==None)
            log("Could not spawn security for player "$NewPlayer,'Security');
    }
    else if (SecurityClass == "")
        log("No value for Engine.GameInfo.SecurityClass -- System is not secure.",'Security');
    else
        log("Unknown security class ["$SecurityClass$"] -- System is not secure.",'Security');

    if ( bAttractCam )
        NewPlayer.GotoState('AttractMode');
    else
        NewPlayer.GotoState('Spectating');

    // Init player's name
    if( InName=="" )
        InName=DefaultPlayerName;
    if( Level.NetMode!=NM_Standalone || NewPlayer.PlayerReplicationInfo.PlayerName==DefaultPlayerName )
        ChangeName( NewPlayer, InName, false );

    // custom voicepack
    NewPlayer.PlayerReplicationInfo.VoiceTypeName = ParseOption ( Options, "Voice");

    InCharacter = ParseOption(Options, "Character");
    NewPlayer.SetPawnClass(DefaultPlayerClassName, InCharacter);
    InSex = ParseOption(Options, "Sex");
    if ( Left(InSex,3) ~= "F" )
        NewPlayer.SetPawnFemale();  // only effective if character not valid

    // Set the player's ID.
    NewPlayer.PlayerReplicationInfo.PlayerID = CurrentID++;

    if ( bSpectator || NewPlayer.PlayerReplicationInfo.bOnlySpectator || !ChangeTeam(newPlayer, InTeam, false) )
    {
        NewPlayer.PlayerReplicationInfo.bOnlySpectator = true;
        NewPlayer.PlayerReplicationInfo.bIsSpectator = true;
        NewPlayer.PlayerReplicationInfo.bOutOfLives = true;
        NumSpectators++;

        return NewPlayer;
    }

    newPlayer.StartSpot = StartSpot;

    // Init player's administrative privileges and log it
    if (AccessControl != None && AccessControl.AdminLogin(NewPlayer, InAdminName, InPassword))
    {
        AccessControl.AdminEntered(NewPlayer, InAdminName);
    }

    NumPlayers++;
    if ( NumPlayers > 20 )
        bLargeGameVOIP = true;
    bWelcomePending = true;

    if ( bTestMode )
        TestLevel();

    // if delayed start, don't give a pawn to the player yet
    // Normal for multiplayer games
    if ( bDelayedStart )
    {
        NewPlayer.GotoState('PlayerWaiting');
        return NewPlayer;
    }
    return newPlayer;
}

function TestLevel()
{
    local Actor A, Found;
    local bool bFoundErrors;

    ForEach AllActors(class'Actor', A)
    {
        bFoundErrors = bFoundErrors || A.CheckForErrors();
        if ( bFoundErrors && (Found == None) )
            Found = A;
    }

    if ( bFoundErrors )
        Found.Crash();
}

/* StartMatch()
Start the game - inform all actors that the match is starting, and spawn player pawns
*/
function StartMatch()
{
    local Controller P;
    local Actor A;

    if (GameStats!=None)
        GameStats.StartGame();

    // tell all actors the game is starting
    ForEach AllActors(class'Actor', A)
        A.MatchStarting();

    // start human players first
    for ( P = Level.ControllerList; P!=None; P=P.nextController )
        if ( P.IsA('PlayerController') && (P.Pawn == None) )
        {
            if ( bGameEnded )
                return; // telefrag ended the game with ridiculous frag limit
            else if ( PlayerController(P).CanRestartPlayer()  )
                RestartPlayer(P);
        }

    // start AI players
    for ( P = Level.ControllerList; P!=None; P=P.nextController )
        if ( P.bIsPlayer && !P.IsA('PlayerController') )
        {
            if ( Level.NetMode == NM_Standalone )
                RestartPlayer(P);
            else
                P.GotoState('Dead','MPStart');
        }

    bWaitingToStartMatch = false;
    GameReplicationInfo.bMatchHasBegun = true;
}

// Player Can be restarted ?
function bool PlayerCanRestart( PlayerController aPlayer )
{
    return true;
}

function bool PlayerCanRestartGame( PlayerController aPlayer )
{
    return true;
}

//
// Restart a player.
//
function RestartPlayer( Controller aPlayer )
{
    local NavigationPoint startSpot;
    local int TeamNum;
    local class<Pawn> DefaultPlayerClass;
    local Vehicle V, Best;
    local vector ViewDir;
    local float BestDist, Dist;

    if( bRestartLevel && Level.NetMode!=NM_DedicatedServer && Level.NetMode!=NM_ListenServer )
        return;

    if ( (aPlayer.PlayerReplicationInfo == None) || (aPlayer.PlayerReplicationInfo.Team == None) )
        TeamNum = 255;
    else
        TeamNum = aPlayer.PlayerReplicationInfo.Team.TeamIndex;

    startSpot = FindPlayerStart(aPlayer, TeamNum);
    if( startSpot == None )
    {
        log(" Player start not found!!!");
        return;
    }

    if (aPlayer.PreviousPawnClass!=None && aPlayer.PawnClass != aPlayer.PreviousPawnClass)
        BaseMutator.PlayerChangedClass(aPlayer);

    if ( aPlayer.PawnClass != None )
        aPlayer.Pawn = Spawn(aPlayer.PawnClass,,,StartSpot.Location,StartSpot.Rotation);

    if( aPlayer.Pawn==None )
    {
        DefaultPlayerClass = GetDefaultPlayerClass(aPlayer);
        aPlayer.Pawn = Spawn(DefaultPlayerClass,,,StartSpot.Location,StartSpot.Rotation);
    }
    if ( aPlayer.Pawn == None )
    {
        log("Couldn't spawn player of type "$aPlayer.PawnClass$" at "$StartSpot);
        aPlayer.GotoState('Dead');
        if ( PlayerController(aPlayer) != None )
            PlayerController(aPlayer).ClientGotoState('Dead','Begin');
        return;
    }
    if ( PlayerController(aPlayer) != None )
        PlayerController(aPlayer).TimeMargin = -0.1;
    aPlayer.Pawn.Anchor = startSpot;
    aPlayer.Pawn.LastStartSpot = PlayerStart(startSpot);
    aPlayer.Pawn.LastStartTime = Level.TimeSeconds;
    aPlayer.PreviousPawnClass = aPlayer.Pawn.Class;

    aPlayer.Possess(aPlayer.Pawn);
    aPlayer.PawnClass = aPlayer.Pawn.Class;

    aPlayer.Pawn.PlayTeleportEffect(true, true);
    aPlayer.ClientSetRotation(aPlayer.Pawn.Rotation);
    AddDefaultInventory(aPlayer.Pawn);
    TriggerEvent( StartSpot.Event, StartSpot, aPlayer.Pawn);

    if ( bAllowVehicles && (Level.NetMode == NM_Standalone) && (PlayerController(aPlayer) != None) )
    {
        // tell bots not to get into nearby vehicles for a little while
        BestDist = 2000;
        ViewDir = vector(aPlayer.Pawn.Rotation);
        for ( V=VehicleList; V!=None; V=V.NextVehicle )
            if ( V.bTeamLocked && (aPlayer.GetTeamNum() == V.Team) )
            {
                Dist = VSize(V.Location - aPlayer.Pawn.Location);
                if ( (ViewDir Dot (V.Location - aPlayer.Pawn.Location)) < 0 )
                    Dist *= 2;
                if ( Dist < BestDist )
                {
                    Best = V;
                    BestDist = Dist;
                }
            }

        if ( Best != None )
            Best.PlayerStartTime = Level.TimeSeconds + 8;
    }
}

function class<Pawn> GetDefaultPlayerClass(Controller C)
{
    local PlayerController PC;
    local String PawnClassName;
    local class<Pawn> PawnClass;

    PC = PlayerController( C );

    if( PC != None )
    {
        PawnClassName = PC.GetDefaultURL( "Class" );
        PawnClass = class<Pawn>( DynamicLoadObject( PawnClassName, class'Class') );

        if( PawnClass != None )
            return( PawnClass );
    }

    return( class<Pawn>( DynamicLoadObject( DefaultPlayerClassName, class'Class' ) ) );
}

//
// Called after a successful login. This is the first place
// it is safe to call replicated functions on the PlayerController.
//
event PostLogin( PlayerController NewPlayer )
{
    local class<HUD> HudClass;
    local class<Scoreboard> ScoreboardClass;

    if ( !bIsSaveGame )
    {
        // Log player's login.
        if (GameStats!=None)
        {
            GameStats.ConnectEvent(NewPlayer.PlayerReplicationInfo);
            GameStats.GameEvent("NameChange",NewPlayer.PlayerReplicationInfo.playername,NewPlayer.PlayerReplicationInfo);
        }

        if ( !bDelayedStart )
        {
            // start match, or let player enter, immediately
            bRestartLevel = false;  // let player spawn once in levels that must be restarted after every death
            if ( bWaitingToStartMatch )
                StartMatch();
            else
                RestartPlayer(newPlayer);
            bRestartLevel = Default.bRestartLevel;
        }
    }

    // tell client what hud and scoreboard to use
    if( HUDType == "" )
        log( "No HUDType specified in GameInfo", 'Log' );
    else
    {
        HudClass = class<HUD>(DynamicLoadObject(HUDType, class'Class'));

        if( HudClass == None )
            log( "Can't find HUD class "$HUDType, 'Error' );
    }

    if( ScoreBoardType != "" )
    {
        ScoreboardClass = class<Scoreboard>(DynamicLoadObject(ScoreBoardType, class'Class'));

        if( ScoreboardClass == None )
            log( "Can't find ScoreBoard class "$ScoreBoardType, 'Error' );
    }
    NewPlayer.ClientSetHUD( HudClass, ScoreboardClass );
    SetWeaponViewShake(NewPlayer);

    if ( bIsSaveGame )
        return;

    if ( NewPlayer.Pawn != None )
        NewPlayer.Pawn.ClientSetRotation(NewPlayer.Pawn.Rotation);

    if( VotingHandler != None )
        VotingHandler.PlayerJoin(NewPlayer);

    if ( AccessControl != None )
        NewPlayer.LoginDelay = AccessControl.LoginDelaySeconds;

    NewPlayer.ClientCapBandwidth(NewPlayer.Player.CurrentNetSpeed);
    NotifyLogin(NewPlayer.PlayerReplicationInfo.PlayerID);

//if _RO_
    if ( Level.NetMode != NM_Client )
    {
        NewPlayer.SteamStatsAndAchievements = Spawn(NewPlayer.default.SteamStatsAndAchievementsClass, NewPlayer);
        if ( !NewPlayer.SteamStatsAndAchievements.Initialize(NewPlayer) )
        {
            NewPlayer.SteamStatsAndAchievements.Destroy();
            NewPlayer.SteamStatsAndAchievements = none;
        }
    }
//endif _RO_

    log("New Player"@NewPlayer.PlayerReplicationInfo.PlayerName@"id="$NewPlayer.GetPlayerIDHash());
}

function SetWeaponViewShake(PlayerController P)
{
    P.ClientSetWeaponViewShake(bWeaponShouldViewShake && bModViewShake);
}

//
// Player exits.
//
function Logout( Controller Exiting )
{
    local bool bMessage;

    bMessage = true;
    if ( PlayerController(Exiting) != None )
    {
        if ( AccessControl != None && AccessControl.AdminLogout( PlayerController(Exiting) ) )
            AccessControl.AdminExited( PlayerController(Exiting) );

        if ( PlayerController(Exiting).PlayerReplicationInfo.bOnlySpectator )
        {
            bMessage = false;
            NumSpectators--;
        }
        else
        {
            NumPlayers--;
        }

//if _RO_
        if ( PlayerController(Exiting).SteamStatsAndAchievements != none )
        {
            PlayerController(Exiting).SteamStatsAndAchievements.Destroy();
        }
//endif _RO_
    }

    if( bMessage && (Level.NetMode==NM_DedicatedServer || Level.NetMode==NM_ListenServer) )
        BroadcastLocalizedMessage(GameMessageClass, 4, Exiting.PlayerReplicationInfo);

    if( VotingHandler != None )
        VotingHandler.PlayerExit(Exiting);

    if ( GameStats!=None)
        GameStats.DisconnectEvent(Exiting.PlayerReplicationInfo);

    //notify mutators that a player exited
    NotifyLogout(Exiting);
}

// Called at the end of GameInfo.PostLogin()
// Notify all existing PlayerControllers that this player has joined.  Each player's ChatManager replicates
// this information to the client for matching against the client's "Restricted" list of players.  If a match is made,
// the client's copy of ChatManager notifies the server's copy so that the corresponding type of communication is not
// sent to the player.
function NotifyLogin(int NewPlayerID)
{
    local int i;
    local array<PlayerController> PCArray;

    GetPlayerControllerList(PCArray);
    for ( i = 0; i < PCArray.Length; i++ )
        PCArray[i].ServerRequestBanInfo(NewPlayerID);
}

function NotifyLogout(Controller Exiting)
{
    local Controller C;
    local PlayerController PC;

    BaseMutator.NotifyLogout(Exiting);

    if ( PlayerController(Exiting) != None && Exiting.PlayerReplicationInfo != None )
    {
        for ( C = Level.ControllerList; C != None; C = C.NextController )
        {
            PC = PlayerController(C);
            if ( PC != None && PC.ChatManager != None )
                PC.ChatManager.UnTrackPlayer(Exiting.PlayerReplicationInfo.PlayerID);
        }
    }
}

//
// Examine the passed player's inventory, and accept or discard each item.
// AcceptInventory needs to gracefully handle the case of some inventory
// being accepted but other inventory not being accepted (such as the default
// weapon).  There are several things that can go wrong: A weapon's
// AmmoType not being accepted but the weapon being accepted -- the weapon
// should be killed off. Or the player's selected inventory item, active
// weapon, etc. not being accepted, leaving the player weaponless or leaving
// the HUD inventory rendering messed up (AcceptInventory should pick another
// applicable weapon/item as current).
//
event AcceptInventory(pawn PlayerPawn)
{
    //default accept all inventory except default weapon (spawned explicitly)
}

function AddGameSpecificInventory(Pawn p)
{
    local Weapon newWeapon;
    local class<Weapon> WeapClass;

    // Spawn default weapon.
    WeapClass = BaseMutator.GetDefaultWeapon();
    if( (WeapClass!=None) && (p.FindInventoryType(WeapClass)==None) )
    {
        newWeapon = Spawn(WeapClass,,,p.Location);
        if( newWeapon != None )
        {
            newWeapon.GiveTo(p);
            newWeapon.bCanThrow = false; // don't allow default weapon to be thrown out
        }
    }
}

//
// Spawn any default inventory for the player.
//
function AddDefaultInventory( pawn PlayerPawn )
{
    local Weapon newWeapon;
    local class<Weapon> WeapClass;

    // Spawn default weapon.
    WeapClass = BaseMutator.GetDefaultWeapon();
    if( (WeapClass!=None) && (PlayerPawn.FindInventoryType(WeapClass)==None) )
    {
        newWeapon = Spawn(WeapClass,,,PlayerPawn.Location);
        if( newWeapon != None )
        {
            newWeapon.GiveTo(PlayerPawn);
            //newWeapon.BringUp();
            newWeapon.bCanThrow = false; // don't allow default weapon to be thrown out
        }
    }
    SetPlayerDefaults(PlayerPawn);
}

/* SetPlayerDefaults()
 first make sure pawn properties are back to default, then give mutators an opportunity
 to modify them
*/
function SetPlayerDefaults(Pawn PlayerPawn)
{
    PlayerPawn.AirControl = PlayerPawn.Default.AirControl;
    PlayerPawn.GroundSpeed = PlayerPawn.Default.GroundSpeed;
    PlayerPawn.WaterSpeed = PlayerPawn.Default.WaterSpeed;
    PlayerPawn.AirSpeed = PlayerPawn.Default.AirSpeed;
    PlayerPawn.Acceleration = PlayerPawn.Default.Acceleration;
    PlayerPawn.JumpZ = PlayerPawn.Default.JumpZ;
    BaseMutator.ModifyPlayer(PlayerPawn);
}

function NotifyKilled(Controller Killer, Controller Killed, Pawn KilledPawn )
{
    local Controller C;

    for ( C=Level.ControllerList; C!=None; C=C.nextController )
        C.NotifyKilled(Killer, Killed, KilledPawn);
}

function KillEvent(string Killtype, PlayerReplicationInfo Killer, PlayerReplicationInfo Victim, class<DamageType> Damage)
{
    if ( GameStats != None )
        GameStats.KillEvent(KillType, Killer, Victim, Damage);
}

function Killed( Controller Killer, Controller Killed, Pawn KilledPawn, class<DamageType> damageType )
{
    local vehicle V;

    if ( (Killed != None) && Killed.bIsPlayer )
    {
        Killed.PlayerReplicationInfo.Deaths += 1;
        Killed.PlayerReplicationInfo.NetUpdateTime = FMin(Killed.PlayerReplicationInfo.NetUpdateTime, Level.TimeSeconds + 0.3 * FRand());
        BroadcastDeathMessage(Killer, Killed, damageType);

        if ( (Killer == Killed) || (Killer == None) )
        {
            if ( Killer == None )
                KillEvent("K", None, Killed.PlayerReplicationInfo, DamageType); //"Kill"
            else
                KillEvent("K", Killer.PlayerReplicationInfo, Killed.PlayerReplicationInfo, DamageType); //"Kill"
        }
        else
        {
            if ( bTeamGame && (Killer.PlayerReplicationInfo != None)
                && (Killer.PlayerReplicationInfo.Team == Killed.PlayerReplicationInfo.Team) )
                KillEvent("TK", Killer.PlayerReplicationInfo, Killed.PlayerReplicationInfo, DamageType);    //"Teamkill"
            else
                KillEvent("K", Killer.PlayerReplicationInfo, Killed.PlayerReplicationInfo, DamageType); //"Kill"
        }
    }
    if ( Killed != None )
        ScoreKill(Killer, Killed);
    DiscardInventory(KilledPawn);
    NotifyKilled(Killer,Killed,KilledPawn);

    if ( bAllowVehicles && (Level.NetMode == NM_Standalone) && (PlayerController(Killed) != None) )
    {
        // tell bots not to get into nearby vehicles
        for ( V=VehicleList; V!=None; V=V.NextVehicle )
            if ( Killed.GetTeamNum() == V.Team )
                V.PlayerStartTime = 0;
    }

}

function bool PreventDeath(Pawn Killed, Controller Killer, class<DamageType> damageType, vector HitLocation)
{
    if ( GameRulesModifiers == None )
        return false;
    return GameRulesModifiers.PreventDeath(Killed,Killer, damageType,HitLocation);
}

function bool PreventSever(Pawn Killed,  Name boneName, int Damage, class<DamageType> DamageType)
{
    if ( GameRulesModifiers == None )
        return false;
    return GameRulesModifiers.PreventSever(Killed, boneName, Damage, DamageType);
}

function BroadcastDeathMessage(Controller Killer, Controller Other, class<DamageType> damageType)
{
    if ( (Killer == Other) || (Killer == None) )
        BroadcastLocalized(self,DeathMessageClass, 1, None, Other.PlayerReplicationInfo, damageType);
    else
        BroadcastLocalized(self,DeathMessageClass, 0, Killer.PlayerReplicationInfo, Other.PlayerReplicationInfo, damageType);
}


// %k = Owner's PlayerName (Killer)
// %o = Other's PlayerName (Victim)
// %w = Owner's Weapon ItemName
static native function string ParseKillMessage( string KillerName, string VictimName, string DeathMessage );

function Kick( string S )
{
    if (AccessControl != None)
        AccessControl.Kick(S);
}

function SessionKickBan( string S ) // sjs
{
    if (AccessControl != None)
        AccessControl.SessionKickBan( S );
}

function KickBan( string S )
{
    if (AccessControl != None)
        AccessControl.KickBan(S);
}

function bool IsOnTeam(Controller Other, int TeamNum)
{
    local int OtherTeam;

    if ( bTeamGame && (Other != None) )
    {
        OtherTeam = Other.GetTeamNum();
        if ( OtherTeam == 255 )
            return false;
        return (OtherTeam == TeamNum);
    }
    return false;
}

//-------------------------------------------------------------------------------------
// Level gameplay modification.

//
// Return whether Viewer is allowed to spectate from the
// point of view of ViewTarget.
//
function bool CanSpectate( PlayerController Viewer, bool bOnlySpectator, actor ViewTarget )
{
    return true;
}

/* Use reduce damage for teamplay modifications, etc.
*/
function int ReduceDamage( int Damage, pawn injured, pawn instigatedBy, vector HitLocation, out vector Momentum, class<DamageType> DamageType )
{
    local int OriginalDamage;
    local armor FirstArmor, NextArmor;

    OriginalDamage = Damage;

    if( injured.PhysicsVolume.bNeutralZone )
        Damage = 0;
    else if ( injured.InGodMode() ) // God mode
        return 0;
    else if ( (injured.Inventory != None) && (damage > 0) ) //then check if carrying armor
    {
        FirstArmor = injured.inventory.PrioritizeArmor(Damage, DamageType, HitLocation);
        while( (FirstArmor != None) && (Damage > 0) )
        {
            NextArmor = FirstArmor.nextArmor;
            Damage = FirstArmor.ArmorAbsorbDamage(Damage, DamageType, HitLocation);
            FirstArmor = NextArmor;
        }
    }

    if ( GameRulesModifiers != None )
        return GameRulesModifiers.NetDamage( OriginalDamage, Damage,injured,instigatedBy,HitLocation,Momentum,DamageType );

    return Damage;
}

//
// Return whether an item should respawn.
//
function bool ShouldRespawn( Pickup Other )
{
    if( Level.NetMode == NM_StandAlone )
        return false;

    return Other.ReSpawnTime!=0.0;
}

/* Called when pawn has a chance to pick Item up (i.e. when
   the pawn touches a weapon pickup). Should return true if
   he wants to pick it up, false if he does not want it.
*/
function bool PickupQuery( Pawn Other, Pickup item )
{
    local byte bAllowPickup;

    if ( (GameRulesModifiers != None) && GameRulesModifiers.OverridePickupQuery(Other, item, bAllowPickup) )
        return (bAllowPickup == 1);

    if ( Other.Inventory == None )
        return true;
    else
        return !Other.Inventory.HandlePickupQuery(Item);
}

/* Discard a player's inventory after he dies.
*/
function DiscardInventory( Pawn Other )
{
    Other.Weapon = None;
    Other.SelectedItem = None;
    while ( Other.Inventory != None )
        Other.Inventory.Destroy();
}

/* Try to change a player's name.
*/
function ChangeName( Controller Other, coerce string S, bool bNameChange )
{
    local Controller C;

    if( S == "" )
        return;

    S = StripColor(s);  // Stip out color codes

    Other.PlayerReplicationInfo.SetPlayerName(S);
    // notify local players
    if ( bNameChange )
        for ( C=Level.ControllerList; C!=None; C=C.NextController )
            if ( (PlayerController(C) != None) && (Viewport(PlayerController(C).Player) != None) )
                PlayerController(C).ReceiveLocalizedMessage( class'GameMessage', 2, Other.PlayerReplicationInfo );
}

/* Return whether a team change is allowed.
*/
function bool ChangeTeam(Controller Other, int N, bool bNewTeam)
{
    return true;
}

/* Return a picked team number if none was specified
*/
function byte PickTeam(byte Current, Controller C)
{
    return Current;
}

/* Send a player to a URL.
*/
function SendPlayer( PlayerController aPlayer, string URL )
{
    aPlayer.ClientTravel( URL, TRAVEL_Relative, true );
}

/* Restart the game.
*/
function RestartGame()
{
    local string NextMap;
    local MapList MyList;

    if ( (GameRulesModifiers != None) && GameRulesModifiers.HandleRestartGame() )
        return;

    if ( bGameRestarted )
        return;
    bGameRestarted = true;

    // allow voting handler to stop travel to next map
    if ( VotingHandler != None && !VotingHandler.HandleRestartGame() )
        return;

    // these server travels should all be relative to the current URL
    if ( bChangeLevels && !bAlreadyChanged && (MapListType != "") )
    {
        // open a the nextmap actor for this game type and get the next map
        bAlreadyChanged = true;
        MyList = GetMapList(MapListType);
        if (MyList != None)
        {
            NextMap = MyList.GetNextMap();
            MyList.Destroy();
        }
        if ( NextMap == "" )
            NextMap = GetMapName(MapPrefix, NextMap,1);

        if ( NextMap != "" )
        {
            Level.ServerTravel(NextMap, false);
            return;
        }
    }

    Level.ServerTravel( "?Restart", false );
}

function array<string> GetMapRotation()
{
    if ( MaplistHandler != None )
        return MaplistHandler.GetCurrentMapRotation();
}

function MapList GetMapList(string MapListClassType)
{
local class<MapList> MapListClass;

    if (MapListClassType != "")
    {
        MapListClass = class<MapList>(DynamicLoadObject(MapListClassType, class'Class'));
        if (MapListClass != None)
            return Spawn(MapListClass);
    }
    return None;
}

function ChangeVoiceChannel( PlayerReplicationInfo PRI, int NewChannelIndex, int OldChannelIndex );

//==========================================================================
// Message broadcasting functions (handled by the BroadCastHandler)

event Broadcast( Actor Sender, coerce string Msg, optional name Type )
{
    BroadcastHandler.Broadcast(Sender,Msg,Type);
}

function BroadcastTeam( Controller Sender, coerce string Msg, optional name Type )
{
    BroadcastHandler.BroadcastTeam(Sender,Msg,Type);
}

/*
 Broadcast a localized message to all players.
 Most message deal with 0 to 2 related PRIs.
 The LocalMessage class defines how the PRI's and optional actor are used.
*/
event BroadcastLocalized( actor Sender, class<LocalMessage> Message, optional int Switch, optional PlayerReplicationInfo RelatedPRI_1, optional PlayerReplicationInfo RelatedPRI_2, optional Object OptionalObject )
{
    BroadcastHandler.AllowBroadcastLocalized(Sender,Message,Switch,RelatedPRI_1,RelatedPRI_2,OptionalObject);
}

//==========================================================================

function bool CheckEndGame(PlayerReplicationInfo Winner, string Reason)
{
    local Controller P;

    if ( (GameRulesModifiers != None) && !GameRulesModifiers.CheckEndGame(Winner, Reason) )
        return false;

    // all player cameras focus on winner or final scene (picked by gamerules)
    for ( P=Level.ControllerList; P!=None; P=P.NextController )
    {
        P.ClientGameEnded();
        P.GameHasEnded();
    }
    return true;
}

/* End of game.
*/
function EndGame( PlayerReplicationInfo Winner, string Reason )
{
    // don't end game if not really ready
    if ( !CheckEndGame(Winner, Reason) )
    {
        bOverTime = true;
        return;
    }

    bGameEnded = true;
    TriggerEvent('EndGame', self, None);
    EndLogging(Reason);
}

function EndLogging(string Reason)
{

    if (GameStats == None)
        return;

    GameStats.EndGame(Reason);
    GameStats.Destroy();
    GameStats = None;
}

/* Return the 'best' player start for this player to start from.
 */
function NavigationPoint FindPlayerStart( Controller Player, optional byte InTeam, optional string incomingName )
{
    local NavigationPoint N, BestStart;
    local Teleporter Tel;
    local float BestRating, NewRating;
    local byte Team;

    // always pick StartSpot at start of match
    if ( (Player != None) && (Player.StartSpot != None) && (Level.NetMode == NM_Standalone)
        && (bWaitingToStartMatch || ((Player.PlayerReplicationInfo != None) && Player.PlayerReplicationInfo.bWaitingPlayer))  )
    {
        return Player.StartSpot;
    }

    if ( GameRulesModifiers != None )
    {
        N = GameRulesModifiers.FindPlayerStart(Player,InTeam,incomingName);
        if ( N != None )
            return N;
    }

    // if incoming start is specified, then just use it
    if( incomingName!="" )
        foreach AllActors( class 'Teleporter', Tel )
            if( string(Tel.Tag)~=incomingName )
                return Tel;

    // use InTeam if player doesn't have a team yet
    if ( (Player != None) && (Player.PlayerReplicationInfo != None) )
    {
        if ( Player.PlayerReplicationInfo.Team != None )
            Team = Player.PlayerReplicationInfo.Team.TeamIndex;
        else
            Team = InTeam;
    }
    else
        Team = InTeam;

    for ( N=Level.NavigationPointList; N!=None; N=N.NextNavigationPoint )
    {
        NewRating = RatePlayerStart(N,Team,Player);
        if ( NewRating > BestRating )
        {
            BestRating = NewRating;
            BestStart = N;
        }
    }

    if ( (BestStart == None) || ((PlayerStart(BestStart) == None) && (Player != None) && Player.bIsPlayer) )
    {
        log("Warning - PATHS NOT DEFINED or NO PLAYERSTART with positive rating");
        BestRating = -100000000;
        ForEach AllActors( class 'NavigationPoint', N )
        {
            NewRating = RatePlayerStart(N,0,Player);
            if ( InventorySpot(N) != None )
                NewRating -= 50;
            NewRating += 20 * FRand();
            if ( NewRating > BestRating )
            {
                BestRating = NewRating;
                BestStart = N;
            }
        }
    }

    return BestStart;
}

/* Rate whether player should choose this NavigationPoint as its start
default implementation is for single player game
*/
function float RatePlayerStart(NavigationPoint N, byte Team, Controller Player)
{
    local PlayerStart P;

    P = PlayerStart(N);
    if ( P != None )
    {
        if ( P.bSinglePlayerStart )
        {
            if ( P.bEnabled )
                return 1000;
            return 20;
        }
        return 10;
    }
    return 0;
}

function ScoreObjective(PlayerReplicationInfo Scorer, float Score)
{
    if ( Scorer != None )
        Scorer.Score += Score;

    if ( GameRulesModifiers != None )
        GameRulesModifiers.ScoreObjective(Scorer, Score);

    CheckScore(Scorer);
}

/* CheckScore()
see if this score means the game ends
*/
function CheckScore(PlayerReplicationInfo Scorer)
{
    if ( (GameRulesModifiers != None) && GameRulesModifiers.CheckScore(Scorer) )
        return;
}

function ScoreEvent(PlayerReplicationInfo Who, float Points, string Desc)
{
    if (GameStats!=None)
        GameStats.ScoreEvent(Who,Points,Desc);
}

function TeamScoreEvent(int Team, float Points, string Desc)
{
    if ( GameStats != None )
        GameStats.TeamScoreEvent(Team, Points, Desc);
}

function ScoreKill(Controller Killer, Controller Other)
{
    if( (killer == Other) || (killer == None) )
    {
        if ( (Other!=None) && (Other.PlayerReplicationInfo != None) )
        {
            Other.PlayerReplicationInfo.Score -= 1;
            Other.PlayerReplicationInfo.NetUpdateTime = Level.TimeSeconds - 1;
            ScoreEvent(Other.PlayerReplicationInfo,-1,"self_frag");
        }
    }
    else if ( killer.PlayerReplicationInfo != None )
    {
        Killer.PlayerReplicationInfo.Score += 1;
        Killer.PlayerReplicationInfo.NetUpdateTime = Level.TimeSeconds - 1;
        Killer.PlayerReplicationInfo.Kills++;
        ScoreEvent(Killer.PlayerReplicationInfo,1,"frag");
    }

    if ( GameRulesModifiers != None )
        GameRulesModifiers.ScoreKill(Killer, Other);

    if ( (Killer != None) || (MaxLives > 0) )
        CheckScore(Killer.PlayerReplicationInfo);
}

function bool TooManyBots(Controller botToRemove)
{
    return false;
}

// Stub
static function Texture GetRandomTeamSymbol(int base) { return None; }

static function string FindTeamDesignation(GameReplicationInfo GRI, actor A)    // Should be subclassed in various team games
{
    return "";
}

// - Given a %X var return the value.

static function string ParseChatPercVar(Mutator BaseMutator, Controller Who, string Cmd)
{
    // Pass along to Mutators
    if (BaseMutator!=None)
        Cmd = BaseMutator.ParseChatPercVar(Who,Cmd);

    if (Who!=None)
        Cmd = Who.ParseChatPercVar(Cmd);

    return Cmd;
}

// - Parse out % vars for various messages

static function string ParseMessageString(Mutator BaseMutator, Controller Who, String Message)
{
    return Message;
}

static function FillPlayInfo(PlayInfo PlayInfo)
{
    Super.FillPlayInfo(PlayInfo);  // Always begin with calling parent

    PlayInfo.AddSetting(default.BotsGroup,   "GameDifficulty",          GetDisplayText("GameDifficulty"),       0, 2, "Select", default.GIPropsExtras[0], "Xb");

    PlayInfo.AddSetting(default.GameGroup,   "GoalScore",               GetDisplayText("GoalScore"),            0, 0, "Text",     "3;0:999");
    PlayInfo.AddSetting(default.GameGroup,   "TimeLimit",               GetDisplayText("TimeLimit"),            0, 0, "Text",     "3;0:999");
    PlayInfo.AddSetting(default.GameGroup,   "MaxLives",                GetDisplayText("MaxLives"),             0, 0, "Text",     "3;0:999");
    PlayInfo.AddSetting(default.GameGroup,   "bWeaponStay",         GetDisplayText("bWeaponStay"),          1, 0, "Check",             ,            ,    ,True);

    PlayInfo.AddSetting(default.RulesGroup,  "bAllowWeaponThrowing",    GetDisplayText("bAllowWeaponThrowing"), 1, 0, "Check",             ,            ,    ,True);
    PlayInfo.AddSetting(default.RulesGroup,  "bAllowBehindView",        GetDisplayText("bAllowBehindview"),     1, 0, "Check",             ,            ,True,True);
    PlayInfo.AddSetting(default.RulesGroup,  "bWeaponShouldViewShake",  GetDisplayText("bWeaponShouldViewShake"),1, 0, "Check",            ,            ,    ,True);

    PlayInfo.AddSetting(default.ServerGroup, "bEnableStatLogging",      GetDisplayText("bEnableStatLogging"),   0, 1, "Check",             ,            ,True);
    PlayInfo.AddSetting(default.ServerGroup, "bAdminCanPause",          GetDisplayText("bAdminCanPause"),       1, 1, "Check",             ,            ,True,True);
    PlayInfo.AddSetting(default.ServerGroup, "MaxSpectators",           GetDisplayText("MaxSpectators"),        1, 1, "Text",      "3;0:32",            ,True,True);
    PlayInfo.AddSetting(default.ServerGroup, "MaxPlayers",              GetDisplayText("MaxPlayers"),           0, 1, "Text",      "3;0:32",            ,True);
    PlayInfo.AddSetting(default.ServerGroup, "MaxIdleTime",         GetDisplayText("MaxIdleTime"),          0, 1, "Text",      "3;0:300",            ,True,True);

    // Add GRI's PIData
    if (default.GameReplicationInfoClass != None)
    {
        default.GameReplicationInfoClass.static.FillPlayInfo(PlayInfo);
        PlayInfo.PopClass();
    }

    if (default.VoiceReplicationInfoClass != None)
    {
        default.VoiceReplicationInfoClass.static.FillPlayInfo(PlayInfo);
        PlayInfo.PopClass();
    }

    if (default.BroadcastClass != None)
        default.BroadcastClass.static.FillPlayInfo(PlayInfo);

    else class'BroadcastHandler'.static.FillPlayInfo(PlayInfo);

    PlayInfo.PopClass();

    if (class'Engine.GameInfo'.default.VotingHandlerClass != None)
    {
        class'Engine.GameInfo'.default.VotingHandlerClass.static.FillPlayInfo(PlayInfo);
        PlayInfo.PopClass();
    }
    else
        log("GameInfo::FillPlayInfo class'Engine.GameInfo'.default.VotingHandlerClass = None");
}

static function string GetDisplayText(string PropName)
{
    switch (PropName)
    {
        case "GameDifficulty":          return default.GIPropsDisplayText[0];
        case "bWeaponStay":             return default.GIPropsDisplayText[1];
        case "MaxSpectators":           return default.GIPropsDisplayText[4];
        case "MaxPlayers":              return default.GIPropsDisplayText[5];
        case "GoalScore":               return default.GIPropsDisplayText[6];
        case "MaxLives":                return default.GIPropsDisplayText[7];
        case "TimeLimit":               return default.GIPropsDisplayText[8];
        case "bEnableStatLogging":      return default.GIPropsDisplayText[9];
        case "bAllowWeaponThrowing":    return default.GIPropsDisplayText[10];
        case "bAllowBehindview":        return default.GIPropsDisplayText[11];
        case "bAdminCanPause":          return default.GIPropsDisplayText[12];
        case "MaxIdleTime":             return default.GIPropsDisplayText[13];
        case "bWeaponShouldViewShake":  return default.GIPropsDisplayText[14];
    }

    return "";
}

static function string GetDescriptionText(string PropName)
{
    switch (PropName)
    {
        case "GameDifficulty":          return default.GIPropDescText[0];
        case "bWeaponStay":             return default.GIPropDescText[1];
        case "MaxSpectators":           return default.GIPropDescText[4];
        case "MaxPlayers":              return default.GIPropDescText[5];
        case "GoalScore":               return default.GIPropDescText[6];
        case "MaxLives":                return default.GIPropDescText[7];
        case "TimeLimit":               return default.GIPropDescText[8];
        case "bEnableStatLogging":      return default.GIPropDescText[9];
        case "bAllowWeaponThrowing":    return default.GIPropDescText[10];
        case "bAllowBehindview":        return default.GIPropDescText[11];
        case "bAdminCanPause":          return default.GIPropDescText[12];
        case "MaxIdleTime":             return default.GIPropDescText[13];
        case "bWeaponShouldViewShake":  return default.GIPropDescText[14];
    }

    return Super.GetDescriptionText(PropName);
}

static event bool AcceptPlayInfoProperty(string PropName)
{
    if ( PropName == "MaxLives" )
        return false;

    return Super.AcceptPlayInfoProperty(PropName);
}

static function int OrderToIndex(int Order)
{
    return Order;
}

function ReviewJumpSpots(name TestLabel);

function string RecommendCombo(string ComboName)
{
    return ComboName;
}

function string NewRecommendCombo(string ComboName, AIController C)
{
    local string NewComboName;

    NewComboName = RecommendCombo(ComboName);
    if (NewComboName != ComboName)
        return NewComboName;

    return BaseMutator.NewRecommendCombo(ComboName, C);
}

function bool CanEnterVehicle(Vehicle V, Pawn P)
{
    return BaseMutator.CanEnterVehicle(V, P);
}

function DriverEnteredVehicle(Vehicle V, Pawn P)
{
    BaseMutator.DriverEnteredVehicle(V, P);
}

function bool CanLeaveVehicle(Vehicle V, Pawn P)
{
    return BaseMutator.CanLeaveVehicle(V, P);
}

function DriverLeftVehicle(Vehicle V, Pawn P)
{
    BaseMutator.DriverLeftVehicle(V, P);
}



function TeamInfo OtherTeam(TeamInfo Requester)
{
    return None;
}

exec function KillBots(int num);

exec function AdminSay(string Msg)
{
    local controller C;

    for( C=Level.ControllerList; C!=None; C=C.nextController )
        if( C.IsA('PlayerController') )
        {
            PlayerController(C).ClearProgressMessages();
            PlayerController(C).SetProgressTime(6);
            PlayerController(C).SetProgressMessage(0, Msg, class'Canvas'.Static.MakeColor(255,255,255));
        }
}

function RegisterVehicle(Vehicle V)
{
    // add to AI vehicle list
    V.NextVehicle = VehicleList;
    VehicleList = V;
}


function actor FindSpecGoalFor(PlayerReplicationInfo PRI, int TeamIndex)
{
    return none;
}

function int GetDefenderNum()
{
    return 255;
}

event SetGrammar()
{
    if( BeaconName!="" )
        LoadSRGrammar(BeaconName);
}

native function LoadSRGrammar( string Grammar );

// Server-only convenience function to retrieve a list of non-AI playercontrollers
function GetPlayerControllerList(out array<PlayerController> ControllerArray)
{
    local Controller C;
    local PlayerController PC;

    if ( ControllerArray.Length > 0 )
        ControllerArray.Remove(0, ControllerArray.Length);


    for (C = Level.ControllerList; C != None; C = C.NextController)
    {
        PC = PlayerController(C);
        if ( PC != None && PC.bIsPlayer && PC.PlayerReplicationInfo != None &&
        !PC.PlayerReplicationInfo.bOnlySpectator && !PC.PlayerReplicationInfo.bBot && PC.Player != None )
            ControllerArray[ControllerArray.Length] = PC;
    }
}

/* Parse voice command and give order to bot
*/
function ParseVoiceCommand( PlayerController Sender, string RecognizedString );

// Stub for UnrealMPGameInfo
static function AdjustBotInterface(bool bSinglePlayer);

// matinee Scene events
event SceneStarted( SceneManager SM, Actor Other );
event SceneEnded( SceneManager SM, Actor Other );
event SceneAbort();                     // Called from PlayerController when pressing fire.

event NoTranslocatorKeyPressed( PlayerController PC );

static function array<string> GetAllLoadHints(optional bool bThisClassOnly);
static function string GetLoadingHint( PlayerController Ref, string MapName, color HintColor )
{
    local string Hint;
    local int Attempt;

    if ( Ref == None )
        return "";

    while ( Hint == "" && ++Attempt < 10 )
        Hint = ParseLoadingHint(GetNextLoadHint(MapName), Ref, HintColor);

    return Hint;
}

static function string ParseLoadingHint(string Hint, PlayerController Ref, color HintColor)
{
    local string CurrentHint, Cmd, Result;
    local int pos;

    pos = InStr(Hint, "%");
    if ( pos == -1 )
        return Hint;

    do
    {
        Cmd = "";
        Result = "";

        CurrentHint $= Left(Hint,pos);
        Hint = Mid(Hint, pos+1);

        pos = InStr(Hint, "%");
        if ( pos == -1 )
            break;

        Cmd = Left(Hint,pos);
        Hint = Mid(Hint,pos+1);
        Result = GetKeyBindName(Cmd,Ref);
        if ( Result == Cmd || Result == "" )
            break;

        CurrentHint $= MakeColorCode(default.BindColor) $ Result $ MakeColorCode(HintColor);
        pos = InStr(Hint, "%");
    } until ( Hint == "" || pos == -1 );

    if ( Result != "" && Result != Cmd )
        return CurrentHint $ Hint;

    return "";
}

static function string GetKeyBindName( string Cmd, PlayerController Ref )
{
    local string BindStr;
    local array<string> Bindings;
    local int i, idx, BestIdx, Weight, BestWeight;

    if ( Ref == None || Cmd == "" )
        return Cmd;

    BestIdx = -1;
    BindStr = Ref.ConsoleCommand("BINDINGTOKEY" @ "\"" $ Cmd $ "\"");
    if ( BindStr != "" )
    {
        Split(BindStr, ",", Bindings);
        if ( Bindings.Length > 0 )
        {
            for ( i = 0; i < Bindings.Length; i++ )
            {
                idx = int(Ref.ConsoleCommand("KEYNUMBER"@Bindings[i]));
                if ( idx != -1 )
                {
                    Weight = GetBindWeight(idx);
                    if ( Weight > BestWeight )
                    {
                        BestWeight = Weight;
                        BestIdx = idx;
                    }
                }
            }

            if ( BestIdx != -1 )
                return Ref.ConsoleCommand("LOCALIZEDKEYNAME"@BestIdx);
        }
    }

    return Cmd;
}

static function string GetNextLoadHint(string MapName)
{
    return "";
}

static function string MakeColorCode(color NewColor)
{
    // Text colours use 1 as 0.
    if(NewColor.R == 0)
        NewColor.R = 1;

    if(NewColor.G == 0)
        NewColor.G = 1;

    if(NewColor.B == 0)
        NewColor.B = 1;

    return Chr(0x1B)$Chr(NewColor.R)$Chr(NewColor.G)$Chr(NewColor.B);
}

static function int GetBindWeight( byte KeyNumber )
{
    // Left/Right mouse
    if ( KeyNumber == 0x01 || KeyNumber == 0x02 )
        return 100;

    // Main number & letter keys
    if ( KeyNumber >= 0x30 && KeyNumber <= 0x5A )
        return 75;

    // Space or Ctrl/Alt/Shift
    if ( KeyNumber==0x20 || (KeyNumber >= 0x10 && KeyNumber <= 0x12) )
        return 50;

    // Arrow keys
    if ( KeyNumber >= 0x25 && KeyNumber <= 0x28 )
        return 45;

    // Middle mouse
    if ( KeyNumber==0x04 )
        return 40;

    // Mouse scroll wheel
    if ( KeyNumber == 0xEC || KeyNumber == 0xED )
        return 35;

    // Home, PgDn, etc.
    if ( KeyNumber == 0x08 || (KeyNumber >= 0x21 && KeyNumber <= 0x28) )
        return 30;

    // Number pad
    if ( KeyNumber >= 0x60 && KeyNumber <= 0x6F )
        return 25;

    return 20;
}

static function bool IsVehicleMutator( string MutatorClassName )
{
    if ( MutatorClassName ~= "Onslaught.MutBigWheels" )
        return true;
    if ( MutatorClassName ~= "Onslaught.MutWheeledVehicleStunts" )
        return true;
    if ( MutatorClassName ~= "Onslaught.MutLightweightVehicles" )
        return true;
    if ( MutatorClassName ~= "OnslaughtFull.MutVehicleArena" )
        return true;
    return false;
}

// Returns whether a mutator should be allowed with this gametype
static function bool AllowMutator( string MutatorClassName )
{
    if ( MutatorClassName == Default.MutatorClass )
        return true;

    if ( !Default.bAllowVehicles && static.IsVehicleMutator(MutatorClassName) )
        return false;

    return !class'LevelInfo'.static.IsDemoBuild();
}

static function AddServerDetail( out ServerResponseLine ServerState, string RuleName, coerce string RuleValue )
{
    local int i;

    i = ServerState.ServerInfo.Length;
    ServerState.ServerInfo.Length = i + 1;

    ServerState.ServerInfo[i].Key = RuleName;
    ServerState.ServerInfo[i].Value = RuleValue;
}


function string StripColor(string s)
{
    local int p;

    p = InStr(s,chr(27));
    while ( p>=0 )
    {
        s = left(s,p)$mid(S,p+4);
        p = InStr(s,Chr(27));
    }

    return s;
}

function bool JustStarted(float T)
{
    return ( Level.TimeSeconds < T );
}


function int MultiMinPlayers()
{
    return 0;
}

// cheat
function WeakObjectives();
function DisableNextObjective();

// hacky.  If you press use while dead, this function will get called.

function DeadUse(PlayerController PC);

static event class<GameInfo> SetGameType( string MapName )
{
    return default.Class;
}

defaultproperties
{
    bAdminCanPause=false
    bDelayedStart=true
    HUDType="Engine.HUD"
    bWaitingToStartMatch=false
    bLoggingGame=False
    MaxPlayers=16
// ifdef _KF_
    GameDifficulty=+2.0
// else
//    GameDifficulty=+1.0
// endif _KF_
    bRestartLevel=True
    bPauseable=True
    bCanChangeSkin=True
    bCanViewOthers=true
    bChangeLevels=True
    AutoAim=0.930000
    GameSpeed=1.000000
    MaxSpectators=2
    DefaultPlayerName="Player"
    GameName="Game"
    GoreLevel=2
    MutatorClass="Engine.Mutator"
    BroadcastHandlerClass="Engine.BroadcastHandler"
    DeathMessageClass=class'LocalMessage'
    bEnableStatLogging=false
    bAllowWeaponThrowing=true
    AccessControlClass="Engine.AccessControl"
    PlayerControllerClassName="Engine.PlayerController"
    GameMessageClass=class'GameMessage'
    GameReplicationInfoClass=class'GameReplicationInfo'
    VoiceReplicationInfoClass=class'VoiceChatReplicationInfo'
    MaplistHandlerClass=class'Engine.MaplistManager'
    FearCostFalloff=+0.95
    bWeaponShouldViewShake=true
    bModViewShake=true
    Acronym="???"
    VotingHandlerType="xVoting.xVotingHandler"
    GIPropsDisplayText(0)="Bot Skill"
    GIPropsDisplayText(1)="Weapons Stay"
    GIPropsDisplayText(2)="Reduce Gore Level"
    GIPropsDisplayText(3)="Game Speed"
    GIPropsDisplayText(4)="Max Spectators"
    GIPropsDisplayText(5)="Max Players"
    GIPropsDisplayText(6)="Goal Score"
    GIPropsDisplayText(7)="Max Lives"
    GIPropsDisplayText(8)="Time Limit"
    GIPropsDisplayText(9)="World Stats Logging"
    GIPropsDisplayText(10)="Allow Weapon Throwing"
    GIPropsDisplayText(11)="Allow Behind View"
    GIPropsDisplayText(12)="Allow Admin Pausing"
    GIPropsDisplayText(13)="Kick Idlers Time"
    GIPropsDisplayText(14)="Weapons shake view"
    GIPropDescText(0)="Set the skill of your bot opponents."
    GIPropDescText(1)="When enabled, weapons will always be available for pickup."
    GIPropDescText(2)="Enable this option to reduce the amount of blood and guts you see."
    GIPropDescText(3)="Controls how fast time passes in the game."
    GIPropDescText(4)="Sets the maximum number of spectators that can watch the game."
    GIPropDescText(5)="Sets the maximum number of players that can join this server."
    GIPropDescText(6)="The game ends when someone reaches this score."
    GIPropDescText(7)="Limits how many times players can respawn after dying."
    GIPropDescText(8)="The game ends after this many minutes of play."
// if _RO_
    GIPropDescText(9)="Enable this option to send game statistics to the Killing Floor global stats server"
// else
    //GIPropDescText(9)="Enable this option to send game statistics to the UT2004 global stats server"
// end if _RO_
    GIPropDescText(10)="When enabled, players can throw their current weapon out."
    GIPropDescText(11)="Controls whether players can switch to a third person view."
    GIPropDescText(12)="Controls whether administrators can pause the game."
    GIPropDescText(13)="Specifies how long to wait before kicking idle player from server."
    GIPropDescText(14)="When enabled, some weapons cause view shaking while firing."
// if _RO_
    GIPropsExtras[0]="0.000000;Untrained;1.000000;Raw recruit;2.000000;Green soldier;3.000000;Front line soldier;4.000000;Experienced soldier;5.000000;Battle-hardened;6.000000;Highly-decorated;7.000000;Party fanatic"
// end if _RO_
    GoreLevelText[0]="No Gore"
    GoreLevelText[1]="Reduced Gore"
    GoreLevelText[2]="Full Gore"
    CurrentID=1
    Callsigns[0]="ALPHA"
    Callsigns[1]="BRAVO"
    Callsigns[2]="CHARLIE"
    Callsigns[3]="DELTA"
    Callsigns[4]="ECHO"
    Callsigns[5]="FOXTROT"
    Callsigns[6]="GOLF"
    Callsigns[7]="HOTEL"
    Callsigns[8]="INDIA"
    Callsigns[9]="JULIET"
    Callsigns[10]="KILO"
    Callsigns[11]="LIMA"
    Callsigns[12]="MIKE"
    Callsigns[13]="NOVEMBER"
    Callsigns[14]="OSCAR"

    MaxIdleTime=+0.0
    NoBindString="[None]"
    BindColor=(R=128,G=128,A=255)
}
