/* CODE BREAKING WARNING in v5.16 Beta 12
Removed AchInfo objects. Used AchDef.CurrentProgress instead

Changed variables and functions:
+ AchStrInfo
! PendingAchievements
- CurrentAchievement
+ CurrentAchHandler, CurrentAchIndex;
! DisplayAchievementStatus(), DisplayCurrentAchievement(), PlayerTick()

ScrnAchievementEarnedMsg - requires ScrnAchievement object to be passed as optional object, switch now indicates ach index

*/


class ScrnPlayerController extends KFPCServ;


var ScrnBalance Mut;
var transient bool bHadPawn;

var byte VeterancyChangeWave; // wave number, when player has changed his perk

var globalconfig bool bManualReload, bOtherPlayerLasersBlue;
var globalconfig bool bAlwaysDisplayAchProgression; // always display a message on any achievement progress
var globalconfig int AchGroupIndex;
var globalconfig int Custom3DScopeSens;

var globalconfig bool bSpeechVote; //allow using speeches Yes and No in voting

struct SWeaponSetting
{
    var globalconfig class<KFWeapon> Weapon;
    var globalconfig byte SkinIndex;
    var transient class<KFWeapon> SkinnedWeapon; //used on server-side only
    var transient KFWeapon LastWeapon; // last weapon on which skin was used
};
var globalconfig array<SWeaponSetting> WeaponSettings;


var transient array<ScrnAchievements.AchStrInfo>PendingAchievements; //earned achievements waiting to be displayed on the HUD
//current achievement object and index to display on the HUD
var transient ScrnAchievements CurrentAchHandler;
var transient int CurrentAchIndex;
var float AchievementDisplayCooldown; // time to wait between displaying achievements, if multiple achievements were earned

var transient class<KFVeterancyTypes> InitialPerkClass;
var bool bChangedPerkDuringGame; // player changed his perk during a game
var transient bool bCowboyForWave;
var transient byte BeggingForMoney; // how many times player asked for money during this wave
var transient bool bShoppedThisWave;
var byte PathDestination; // 0 - trader, 255 - TSC base

var byte MaxVoiceMsgIn10s; // maximum number of voice messages during 10 seconds
var bool bZEDTimeActive;

var bool bHadArmor; //player had armor during the game

var bool bWeaponsLocked; // disables player's weapons pick up by other players
var localized string strLocked, strUnlocked, strLockDisabled;
var transient float LastLockMsgTime; // last time when player received a message that weapon is locked

var localized string strAlreadySpectating;

var globalconfig bool bPrioritizePerkedWeapons, bPrioritizeBoomstick;

var int StartCash; // amount of cash given to this pawn on game/wave start

struct SDualWieldable {
    var class<KFWeapon> Single, Dual;
};
var array<SDualWieldable> DualWieldables;
var private transient bool bDualWieldablesLoaded;


// TSC
var string ProfilePageClassString;
var    string    TSCLobbyMenuClassString;
var config bool bTSCAdvancedKeyBindings; // pressing altfire while carrying the guardian gnome, sets up the base
var config bool bTSCAutoDetectKeys; // turns off bTSCAdvancedKeyBindings if a dedicated key is bound for SetupBase
var config string RedCharacter, BlueCharacter;

// not replicated yet
// todo: find an efficient way to replicate
var array<string> RedCharacters, BlueCharacters;

var class<ScrnCustomPRI> CustomPlayerReplicationInfoClass;
var ScrnCustomPRI ScrnCustomPRI;
var transient bool bDestroying; // indicates that Destroyed() is executing

var bool bDamageAck; // does server needs to acknowledge client of damages he made?

var transient byte PerkChangeWave; // wave num, when perk was changed last time
var    localized string strNoPerkChanges, strPerkLocked;

var transient Controller FavoriteSpecs[2];
var localized string strSpecFavoriteAssigned, strSpecNoFavorites;
var transient Actor OldViewTarget;

var transient rotator PrevRot;
var transient int ab_warning;

var globalconfig bool bDebugRepLink, bWaveGarbageCollection;
var globalconfig string PlayerName;

// DLC checks
struct SDLC {
    var int AppID;
    var bool bOwnsDLC;
    var bool bChecked;
};
var array<SDLC> DLC;

var transient int AchResetPassword;


struct SMusicRecord {
    var byte PL, Wave;
    var bool bTrader;
    var string Song;
    var string Artist, Title;
};
var globalconfig byte ActiveMusicPlaylist;
var globalconfig array<String> MusicPlaylistNames;
var globalconfig array<SMusicRecord> MyMusic;

var private transient array<int> MusicPlaylistIndex; // MusicPlaylistIndex[PL] = index of first playlist entry in MyMusic
var private transient bool bMusicPlaylistIndexReady;
var private transient int ActiveMusicSongIndex;
var private transient string PendingSong;
var localized string strPlayingSong;

var class<ScrnFlareCloud> FlareCloudClass;


replication
{
    reliable if ( Role == ROLE_Authority )
        ClientMonsterBlamed, ClientPostLogin;

    unreliable if ( bDamageAck && Role == ROLE_Authority )
        ClientPlayerDamaged;

    unreliable if ( Role == ROLE_Authority )
        ClientFlareDamage;

    reliable if ( Role < ROLE_Authority )
        SrvAchReset, ResetMyAchievements, ResetMapAch,
        ServerDropAllWeapons, ServerLockWeapons, ServerGunSkin,
        ServerAcknowledgeDamages, ServerShowPathTo,
        ServerDebugRepLink, ServerFixQuickMelee,
        ServerTourneyCheck, ServerKillMut, ServerKillRules,
        ServerSwitchViewMode, ServerSetViewTarget;

    reliable if ( bNetOwner && (bNetDirty || bNetInitial) && Role < ROLE_Authority )
        bPrioritizePerkedWeapons, StartCash;

    // TSC stuff
    reliable if ( Role < ROLE_Authority )
        ServerScoreFlag;
}

simulated function PreBeginPlay()
{
    super.PreBeginPlay();

    class'ScrnMagnum44Fire'.static.PreloadAssets(Level);
    class'ScrnDual44MagnumFire'.static.PreloadAssets(Level);
    class'ScrnMAC10Fire'.static.PreloadAssets(Level);
    class'KFMod.M79Fire'.static.PreloadAssets(Level);
}


// this function is called on on server side only
// and only if using ScrnGameType, ScrnStoryGameInfo or descendants
function PostLogin()
{
    ClientPostLogin();

    ScrnCustomPRI.GotoState('');
    if ( Level.NetMode == NM_Standalone || (Level.NetMode == NM_ListenServer && Viewport(Player) != None) )
        ScrnCustomPRI.SetSteamID64(Mut.MySteamID64);
    else
        ScrnCustomPRI.SetSteamID64(GetPlayerIDHash());
    ScrnCustomPRI.NetUpdateTime = Level.TimeSeconds - 1;

    if ( PlayerName != "" )
        SetName(PlayerName);

    Mut.GameRules.PlayerEntering(self);
}

simulated function ClientPostLogin()
{
    // DLC check moved to ScrnClientPerkRepLink
    // if ( Viewport(Player) != None )  {
        // CheckDLC();
    // }
}


simulated function LoadMutSettings()
{
    if ( Mut != none ) {
        if ( Mut.bForceManualReload)
            bManualReload = Mut.bManualReload;
        if ( Mut.bHardCore )
            bOtherPlayerLasersBlue = false;
        if ( ScrnHumanPawn(Pawn) != none )
            ScrnHumanPawn(Pawn).bTraderSpeedBoost = Mut.bTraderSpeedBoost;
    }
    else {
        //this shouldn't happen
        log("Player Controller can not find ScrnBalance!", 'ScrnBalance');
        if ( class'ScrnBalance'.default.Mut.bForceManualReload)
            bManualReload =  class'ScrnBalance'.default.bManualReload;
        if ( class'ScrnBalance'.default.Mut.bHardcore )
            bOtherPlayerLasersBlue = false;
    }
}

function InitPlayerReplicationInfo()
{
    local LinkedReplicationInfo L;

    super.InitPlayerReplicationInfo();

    for( L=PlayerReplicationInfo.CustomReplicationInfo; L!=None; L=L.NextReplicationInfo ) {
        if ( ScrnCustomPRI(L) != none )
            return; // wtf?
        if( L.NextReplicationInfo==None )
            break;
    }

    ScrnCustomPRI = spawn(CustomPlayerReplicationInfoClass, self);
    if ( ScrnCustomPRI != none ) {
        if ( PlayerReplicationInfo.CustomReplicationInfo != none )
            ScrnCustomPRI.NextReplicationInfo = PlayerReplicationInfo.CustomReplicationInfo;
        PlayerReplicationInfo.CustomReplicationInfo = ScrnCustomPRI;
    }
    else
        warn("Can not create ScrnCustomPRI: " $ CustomPlayerReplicationInfoClass);
}

//copypaste from KFPlayerController with constant value of 24 replaced by Custom3DScopeSens to set custom sensitivity for 3D scopes
function float GetMouseModifier()
{
    local KFWeapon weap;

    if (Pawn == none || Pawn.Weapon == none)
        return -1.0;

    weap = KFWeapon(Pawn.Weapon);

    if (weap== none )
        return -1.0;

    if(weap.KFScopeDetail == KF_ModelScope && weap.ShouldDrawPortal())
    {
        return Custom3DScopeSens;
    }
    else if(weap.KFScopeDetail == KF_ModelScopeHigh && weap.ShouldDrawPortal())
    {
        return Custom3DScopeSens;
    }
    else
    {
        return -1.0;
    }
}

simulated function ClientForceCollectGarbage()
{
    ConsoleCommand("MEMSTAT", true); // write memory stats into log
    if ( bWaveGarbageCollection ) {
        super.ClientForceCollectGarbage();
        ConsoleCommand("MEMSTAT", true);
    }
}


simulated function PreloadFireModeAssetsSE(class<WeaponFire> WF, optional WeaponFire SpawnedFire)
{
    local class<Projectile> P;//log("ScrnPlayerController.PreloadFireModeAssets()" @ WF, 'ScrnBalance');

    if ( WF == none || WF == Class'KFMod.NoFire' )
        return;


    if ( class<KFFire>(WF) != none && class<KFFire>(WF).default.FireSoundRef != "" )
        class<KFFire>(WF).static.PreloadAssets(Level, KFFire(SpawnedFire));
    else if ( class<KFMeleeFire>(WF) != none && class<KFMeleeFire>(WF).default.FireSoundRef != "" )
        class<KFMeleeFire>(WF).static.PreloadAssets(KFMeleeFire(SpawnedFire));
    else if ( class<KFShotgunFire>(WF) != none && class<KFShotgunFire>(WF).default.FireSoundRef != "" )
        class<KFShotgunFire>(WF).static.PreloadAssets(Level, KFShotgunFire(SpawnedFire));


    // preload projectile assets
    P = WF.default.ProjectileClass;
    //log("Projectile =" @ P, 'ScrnBalance');
    if ( P == none )
        return;

    if ( class<ScrnCustomShotgunBullet>(P) != none )
        class<ScrnCustomShotgunBullet>(P).static.PreloadAssets();
    else if ( class<CrossbuzzsawBlade>(P) != none )
        class<CrossbuzzsawBlade>(P).static.PreloadAssets();
    else if ( class<LAWProj>(P) != none && class<LAWProj>(P).default.StaticMeshRef != "" )
        class<LAWProj>(P).static.PreloadAssets();
    else if ( class<M79GrenadeProjectile>(P) != none && class<M79GrenadeProjectile>(P).default.StaticMeshRef != "" )
        class<M79GrenadeProjectile>(P).static.PreloadAssets();
    else if ( class<SPGrenadeProjectile>(P) != none && class<SPGrenadeProjectile>(P).default.StaticMeshRef != "" )
        class<SPGrenadeProjectile>(P).static.PreloadAssets();
    else if ( class<HealingProjectile>(P) != none && class<HealingProjectile>(P).default.StaticMeshRef != "" )
        class<HealingProjectile>(P).static.PreloadAssets();
    else if ( class<CrossbowArrow>(P) != none && class<CrossbowArrow>(P).default.MeshRef != "" )
        class<CrossbowArrow>(P).static.PreloadAssets();
    else if ( class<M99Bullet>(P) != none )
        class<M99Bullet>(P).static.PreloadAssets();
    else if ( class<PipeBombProjectile>(P) != none && class<PipeBombProjectile>(P).default.StaticMeshRef != "" )
        class<PipeBombProjectile>(P).static.PreloadAssets();
    // More DLC
    else if ( class<SealSquealProjectile>(P) != none && class<SealSquealProjectile>(P).default.StaticMeshRef != "" )
        class<SealSquealProjectile>(P).static.PreloadAssets();
}

simulated function ClientWeaponSpawned(class<Weapon> WClass, Inventory Inv)
{
    local class<KFWeapon> W;
    local class<KFWeaponAttachment> Att;
    local Weapon Spawned;

    //log("ScrnPlayerController.ClientWeaponSpawned()" @ WClass $ ". Default Mesh = " $ WClass.default.Mesh, 'ScrnBalance');
    //super.ClientWeaponSpawned(WClass, Inv);

    W = class<KFWeapon>(WClass);
    // preload assets only for weapons that have no static ones
    // damned Tripwire's code doesn't bother for cheking is there ref set or not!
    if ( W != none) {
        //preload weapon assets
        if ( W.default.Mesh == none )
            W.static.PreloadAssets(Inv);
        Att = class<KFWeaponAttachment>(W.default.AttachmentClass);
        // 2013/01/22 EDIT: bug fix
        if ( Att != none && Att.default.Mesh == none ) {
            if ( Inv != none )
                Att.static.PreloadAssets(KFWeaponAttachment(Inv.ThirdPersonActor));
            else
                Att.static.PreloadAssets();
        }
        // 2014-11-23 fix
        Spawned = Weapon(Inv);
        if ( Spawned != none ) {
            PreloadFireModeAssetsSE(W.default.FireModeClass[0], Spawned.GetFireMode(0));
            PreloadFireModeAssetsSE(W.default.FireModeClass[1], Spawned.GetFireMode(0));
        }
        else {
            PreloadFireModeAssetsSE(W.default.FireModeClass[0]);
            PreloadFireModeAssetsSE(W.default.FireModeClass[1]);
        }
    }
}





/*
function AcknowledgePossession(Pawn P)
{
    // make sure old pawn controller is right
    if ( Pawn != AcknowledgedPawn ) {
        if( ScrnHumanPawn(AcknowledgedPawn) != none )
            ScrnHumanPawn(AcknowledgedPawn).ScrnPlayer = none;
    }
    if( ScrnHumanPawn(P) != none )  {
        ScrnHumanPawn(P).ScrnPlayer = self;
    }

    super.AcknowledgePossession(P);
}

function ServerAcknowledgePossession(Pawn P, float NewHand, bool bNewAutoTaunt)
{
    // make sure old pawn controller is right
    if ( Pawn != AcknowledgedPawn ) {
        if( ScrnHumanPawn(AcknowledgedPawn) != none )
            ScrnHumanPawn(AcknowledgedPawn).ScrnPlayer = none;
    }
    if( ScrnHumanPawn(P) != none )  {
        ScrnHumanPawn(P).ScrnPlayer = self;
    }

    super.ServerAcknowledgePossession(P, NewHand, bNewAutoTaunt);
}

*/

function PawnDied(Pawn P)
{
    ServerDropFlag();

    super.PawnDied(P);
}


function ShowBuyMenu(string wlTag,float maxweight)
{
    StopForceFeedback();

    ClientOpenMenu(string(Class'ScrnGUIBuyMenu'),,wlTag,string(maxweight));
}


exec function CookGrenade()
{
    if ( ScrnHumanPawn(Pawn) != none )
        ScrnHumanPawn(Pawn).CookGrenade();
}

exec function ThrowCookedGrenade()
{
    if ( ScrnHumanPawn(Pawn) != none )
        ScrnHumanPawn(Pawn).ThrowCookedGrenade();
}

exec function QuickMelee()
{
    if ( ScrnHumanPawn(Pawn) != none )
        ScrnHumanPawn(Pawn).QuickMelee();
}

/**
 * DamTypeNum:
 * 0 - normal
 * 1 - headshot
 * 2 - fire DoT
 */
function DamageMade(int Damage, vector HitLocation, byte DamTypeNum)
{
    local ScrnPlayerController ScrnPC;
    local int i;

    ClientPlayerDamaged(Damage, HitLocation, DamTypeNum);

    if ( Role == ROLE_Authority && ScrnHumanPawn(Pawn) != none && ScrnHumanPawn(Pawn).bViewTarget ) {
        // show damage popups on spectating players
        for ( i=0; i<Level.GRI.PRIArray.length; ++i ) {
            if ( Level.GRI.PRIArray[i] != none && Level.GRI.PRIArray[i].bOnlySpectator ) {
                ScrnPC = ScrnPlayerController(Level.GRI.PRIArray[i].Owner);
                if ( ScrnPC != none && ScrnPC != self && ScrnPC.ViewTarget == Pawn )
                    ScrnPC.ClientPlayerDamaged(Damage, HitLocation, DamTypeNum);
            }
        }
    }
}

simulated function ClientPlayerDamaged(int Damage, vector HitLocation, byte DamTypeNum)
{
    local ScrnHUD hud;

    hud = ScrnHUD(myHUD);
    if ( hud != none ) {
        if ( hud.bShowDamages )
            hud.ShowDamage(Damage, Level.TimeSeconds, HitLocation, DamTypeNum);
        else
            ServerAcknowledgeDamages(false); // tell server we don't need damage acks
    }
}

simulated function ClientFlareDamage(KFMonster Victim, byte DamageX4, byte BurnTime)
{
    local ScrnFlareCloud cloud;



    if ( Level.NetMode == NM_DedicatedServer )
        return;
    if ( Victim == none )
        return; // victim is not replicated

    cloud = Spawn(FlareCloudClass, Victim,, Victim.Location, rotator(vect(0,0,1)));
    cloud.SetDamage(DamageX4 * 4);
    cloud.SetLifeSpan(BurnTime);
}

function ServerAcknowledgeDamages(bool bWantDamage)
{
    bDamageAck = bWantDamage;
}

simulated function ClientMonsterBlamed(class<KFMonster> BlamedMonsterClass)
{
    local ScrnHUD hud;

    hud = ScrnHUD(myHUD);
    if (  hud != none ) {
        hud.BlameCountdown = hud.default.BlameCountdown;
        hud.BlamedMonsterClass = BlamedMonsterClass;
    }
}

exec function ScrnInit()
{
    local LinkedReplicationInfo L;

    // reload ScrnBalance settings
    Mut.InitSettings();

    // reload custom weapon bonuses
    for ( L = Mut.CustomWeaponLink; L != none; L = L.NextReplicationInfo ) {
        ScrnCustomWeaponLink(L).LoadWeaponBonuses();
    }
}


simulated function PlayerTick( float DeltaTime )
{
    local float y, p;

    super.PlayerTick(DeltaTime);

    //log("Player Controller Tick", 'ScrnBalance');
    if ( Viewport(Player) != None ) {
        AchievementDisplayCooldown -= DeltaTime;
        if ( PendingAchievements.Length > 0 && AchievementDisplayCooldown <= 0 ) {
            CurrentAchHandler = PendingAchievements[0].AchHandler;
            CurrentAchIndex = PendingAchievements[0].AchIndex;
            PendingAchievements.remove(0, 1);
            DisplayCurrentAchievement();
        }

        if ( Pawn != none && Pawn.Health > 0 ) {
            if ( PrevRot.Yaw != 0 ) {
                y = abs(Rotation.Yaw - PrevRot.Yaw);
                p = abs(Rotation.Yaw - PrevRot.Yaw);
                if ( y<10000 && p<10000 && p+y>2500 /*&& (p*p + y*y) > 4000000*/ ) {
                    if ( IsAimingToZedHead() )
                        ab_warning+=10; // you made inssanely quick mouse movement and pointed exactly into enemy head. What are you, aimbot?..
                    else
                        ab_warning--; // nah, seems like you need to reduce mouse sensitivity...
                }
            }
            PrevRot = Rotation;
        }

        // set typing to true also when console is open
        if ( Player.Console != none ) {
            bIsTyping = Player.Console.bVisible || Player.Console.bTyping || Player.GUIController.bActive;
            if ( Pawn != none && bIsTyping != Pawn.bIsTyping )
                Typing(bIsTyping); // replicate state to server
        }

        if ( KFInterAct != none && PendingSong != "" && PendingSong == KFInterAct.ActiveSong ) {
            // don't bother with this, if player has turned off music
            if ( GetMusicVolume() > 0 ) {
                if ( KFInterAct.ActiveHandel == 0) {
                    // unable to start song - play default one
                    ClientMessage("Unable to play music file: '"$PendingSong$".ogg'. Playing default song instead.", 'Log');
                    PlayDefaultMusic(class'ScrnMusicTrigger');
                }
                else
                    SongPlaying(PendingSong, ActiveMusicSongIndex);
            }
            PendingSong = "";
        }
    }
}

// TraderMenuOpened and TraderMenuClosed execute on client-side only
simulated function TraderMenuOpened()
{
    bShopping = true;
}

simulated function TraderMenuClosed()
{
    if ( Mut.bTSCGame )
        ServerShowPathTo(1); // go to TSC base
    bShopping = false;
}

function SetShowPathToTrader(bool bShouldShowPath)
{
    if ( bShouldShowPath )
        PathDestination = 0; // trader
    else if ( PathDestination == 0)
        PathDestination = 255; // turn off only if we are showing path to trader (PathDestination = 0)
    ServerShowPathTo(PathDestination);
}

exec function TogglePathToTrader()
{
    switch (PathDestination) {
        case 0:
            PathDestination = 1; // TSC base
            if ( Mut.bTSCGame )
                break; // otherwise go to next steap
        case 1:
            PathDestination = 255; // hide
            break;
        default:
            PathDestination = 0; // trader
    }
    ServerShowPathTo(PathDestination);
}

function ServerShowPathTo(byte NewDestination)
{
    PathDestination = NewDestination;
    SetTimer(TraderPathInterval, true);
    Timer();
}

// Show the path the trader
function Timer()
{
    if( Role < ROLE_Authority )
    {
        SetTimer(0, false);
    }
    else if ( bWantsTraderPath && PathDestination != 255 )
    {
        UnrealMPGameInfo(Level.Game).ShowPathTo(Self, PathDestination);
    }
    else
    {
        bShowTraderPath = false;
        SetTimer(0, false);
    }
}

event SongPlaying(String Song, int MyIndex)
{
    if ( Song == "" )
        return;

    if ( MyIndex >=0 && MyIndex < MyMusic.length && MyMusic[MyIndex].Song == Song ) {
        if ( MyMusic[MyIndex].Title == "" )
            MyMusic[MyIndex].Title = MyMusic[MyIndex].Song;
        ClientMessage(strPlayingSong
            $ ConsoleColorString(MyMusic[MyIndex].Artist, 192, 1, 192)
            @ ConsoleColorString(MyMusic[MyIndex].Title, 255, 255, 1)
        );
    }
    else {
        for ( MyIndex = 0; MyIndex < MyMusic.length; ++MyIndex ) {
            if ( MyMusic[MyIndex].Song == Song )
                SongPlaying(Song, MyIndex);
        }
    }
}

function UpdateMusicPlaylistIndex()
{
    local int i, prev_pl;

    bMusicPlaylistIndexReady = true;
    for ( i=0; i<MyMusic.length; ++i ) {
        if ( MyMusic[i].PL != prev_pl ) {
            if ( MyMusic[i].PL < prev_pl ) {
                ClientMessage("MyMusic broken! MyMusic must be sorded in (PL,Wave,bTrader) order! Line Number = " $i, 'Log');
                MusicPlaylistIndex.Length = 0;
                return;
            }
            prev_pl = MyMusic[i].PL;
            MusicPlaylistIndex[prev_pl] = i;
        }
    }
}

/**
 *  Gets song name from MyMusic according to playlist (PL) and wave numbers.
 *  Function doesn't check if music file exists.
 *
 *  @param PL       Playlist number as defined in MyMusic.PL. Starting with 1. PL=0 is reserved for default music.
 *                  Calling GetMySong with PL = 0 always returns empty string.
 *  @param Wave     Player-friendly wave number, i.e. 1 - Wave 1, 11 - Boss wave etc.
 *                  Calling GetMySong with Wave=0 returns default song for this playlist
 *  @param bTrader  Are we looking for calm song for trader time (bTrader=True) or battle song (false)?
 *  @return         Record index in MyMusic. -1 if not found
 */
function int GetMySongIndex(byte PL, byte Wave, bool bTrader)
{
    local int i, start, end;
    local bool bFound;

    if ( !bMusicPlaylistIndexReady )
        UpdateMusicPlaylistIndex();

    if ( PL == 0 )
        return -1;

    if ( PL >= MusicPlaylistIndex.Length ) {
        if ( ActiveMusicPlaylist == PL )
            ActiveMusicPlaylist = 0;
        ClientMessage("Music Playlist #"$PL $" is not defined", 'Log');
        return -1;
    }

    start = MusicPlaylistIndex[PL];
    if ( start >= MyMusic.Length ) {
        ClientMessage("Music Playlist #"$PL $" has no songs or MyMusic is not sorted", 'Log');
        return -1;
    }
    if ( (PL+1) == MusicPlaylistIndex.Length )
        end = MyMusic.length;
    else
        end = MusicPlaylistIndex[PL+1];

    for ( i = start; i < end; ++i ) {
        if ( bFound ) {
            if ( MyMusic[i].Wave != Wave || MyMusic[i].bTrader != bTrader ) {
                end = i;
                break;
            }
        }
        else if ( MyMusic[i].Wave == Wave && MyMusic[i].bTrader == bTrader ) {
            bFound = true;
            start = i;
        }
    }
    if ( bFound ) {
        if ( start + 1 == end )
            return start;
        else
            return start + Rand(end-start);
    }
    else if ( Wave >  0)
        return GetMySongIndex(PL, 0, bTrader); // no songs defined for given wave. return default

    return -1;
}

function int GetMyActiveSongIndex()
{
    local KFGameReplicationInfo KFGRI;
    local byte w;

    KFGRI = KFGameReplicationInfo(Level.GRI);
    if ( KFGRI == none )
        return -1; // wtf?
    if ( KF_StoryGRI(KFGRI) != none ) {
        // story mode
        return -1; // story music must be set explicitly by L.D.
    }

    w = KFGRI.WaveNumber;
    if ( KFGRI.bWaveInProgress ) {
        if ( w == KFGRI.FinalWave && TSCGameReplicationInfo(KFGRI) == none )
            w = 10; // boss battle song
    }
    return GetMySongIndex(ActiveMusicPlaylist, w+1, !KFGRI.bWaveInProgress);
}

function PlayDefaultMusic(class<KFMusicTrigger> M)
{
    local string sng;

    if ( KFInterAct == none )
        return;

    sng = GetSongFromMusicTrigger(M);
    if ( sng != "" )
        KFInterAct.SetSong(sng, M.default.FadeInTime, M.default.FadeOutTime);
}

function string GetSongFromMusicTrigger(class<KFMusicTrigger> M)
{
    local KFGameReplicationInfo KFGRI;
    local string sng;
    local int w;

    KFGRI = KFGameReplicationInfo(Level.GRI);
    if ( KFGRI == none )
        return sng; // wtf?
    if ( KF_StoryGRI(KFGRI) != none )
        return sng; // story music must be set explicitly by L.D.

    w = KFGRI.WaveNumber;
    if ( KFGRI.bWaveInProgress ) {
        if ( w == KFGRI.FinalWave && M.default.WaveBasedSongs.length > 10 && TSCGameReplicationInfo(KFGRI) == none )
            sng = M.default.WaveBasedSongs[10].CombatSong; // boss battle song
        else if ( w < M.default.WaveBasedSongs.length )
            sng = M.default.WaveBasedSongs[w].CombatSong;
        else
            sng = M.default.CombatSong;
    }
    else {
        if ( KFGRI.TimeToNextWave > 10 )
            w++; // KFGRI.WaveNumber is updated only at the end of the trader time
        if ( w < M.default.WaveBasedSongs.length )
            sng = M.default.WaveBasedSongs[w].CalmSong;
        else
            sng = M.default.Song;
    }
    return sng;
}

function int GetActiveMusicSongIndex()
{
    return ActiveMusicSongIndex;
}

function NetPlayMusic( string Song, float FadeInTime, float FadeOutTime )
{
    if ( ActiveMusicPlaylist > 0 ) {
        ActiveMusicSongIndex = GetMyActiveSongIndex();
        if ( ActiveMusicSongIndex != -1 )
            Song = MyMusic[ActiveMusicSongIndex].Song;
    }
    else
        ActiveMusicSongIndex = -1;

    PendingSong = Song;
    super.NetPlayMusic(Song, FadeInTime, FadeOutTime);
}

exec function PlayMyMusic(int PL)
{
    local int i;

    if ( !bMusicPlaylistIndexReady )
        UpdateMusicPlaylistIndex();

    if ( MusicPlaylistIndex.length <= 1 ) {
        ConsoleMessage("No user-defined music available", 0, 255, 1, 1);
        return;
    }

    if ( PL >= MusicPlaylistIndex.length ) {
        ConsoleMessage("Wrong playlist number!", 0, 255, 1, 1);
        PL = 0;
    }

    if ( PL == 0) {
        ConsoleMessage("Available music playlists:");
        ConsoleMessage("---------------------------------------------");
        for ( i=1; i<MusicPlaylistIndex.length; ++i ) {
            if ( ActiveMusicPlaylist == i )
                ConsoleMessage(i @ MusicPlaylistNames[i], 0, 1, 255, 1);
            else
                ConsoleMessage(i @ MusicPlaylistNames[i]);
        }
        ConsoleMessage("---------------------------------------------");
        if ( ActiveMusicSongIndex != -1 )
             ConsoleMessage(strPlayingSong
                $ ConsoleColorString(MyMusic[ActiveMusicSongIndex].Artist, 192, 1, 192)
                @ ConsoleColorString(MyMusic[ActiveMusicSongIndex].Title, 255, 255, 1)
            );
        return;
    }

    ActiveMusicPlaylist = PL;
    NetPlayMusic("ThisWillBeReplacedWithDefaultWaveTrack", 1, 1);
    ClientMessage(ConsoleColorString(MusicPlaylistNames[PL], 1, 192, 1)
        $ ConsoleColorString(" activated", 192, 192, 192));
    SaveConfig();
}

exec function StopMyMusic(int PL)
{
    if ( ActiveMusicPlaylist > 0 ) {
        ActiveMusicPlaylist = 0;
        ClientMessage("User-defined music disabled. Map-specific music will be restored next wave.");
        SaveConfig();
    }
}

function float GetMusicVolume()
{
    return float(ConsoleCommand("get ini:Engine.Engine.AudioDevice MusicVolume"));
}

exec function MusicVolume(coerce string vol)
{
    if ( vol == "" )
        ClientMessage("Music Volume = " $ ConsoleCommand("get ini:Engine.Engine.AudioDevice MusicVolume"));
    else {
        ConsoleCommand("set ini:Engine.Engine.AudioDevice MusicVolume" @ vol);
        ClientMessage("Music Volume set to " $ ConsoleCommand("get ini:Engine.Engine.AudioDevice MusicVolume"));
    }
}

exec function SoundVolume(coerce string vol)
{
    if ( vol == "" )
        ClientMessage("Sound Volume = " $ ConsoleCommand("get ini:Engine.Engine.AudioDevice SoundVolume"));
    else {
        ConsoleCommand("set ini:Engine.Engine.AudioDevice SoundVolume" @ vol);
        ClientMessage("Sound Volume set to " $ ConsoleCommand("get ini:Engine.Engine.AudioDevice SoundVolume"));
    }
}


function bool IsAimingToZedHead()
{
    local Vector vNull, StartTrace, HeadLoc, LookDir, HeadDir;
    local KFMonster HitMonster;
    local float cosine;

    if ( Pawn == none )
        return false;

    StartTrace = Pawn.Location + Pawn.EyePosition();  // A
    LookDir = Normal(Vector(Rotation)); // B-A

    foreach CollidingActors(class'KFMonster', HitMonster, 2500, StartTrace) {
        HeadLoc = HitMonster.GetBoneCoords('head').Origin;
        if ( HeadLoc == vNull )
            continue; // no head

        if ( !FastTrace(HeadLoc, StartTrace) )
            continue; // head isn't visible

        // I should have better learned geometry in school. It'd really helped :)
        // LookDir - direction where player is lookin
        // HeadDir - direction to the zed's head (HeadLoc)
        // We need to get a distance between HeadLoc and our LookDir line,
        // i.e. perpenducular line from HeadLoc to LookDir line.
        // We get right-angled triangle, where (HeadLoc-StartTrace) is Hypotenuse,
        // LookDir line - Adjacent side and perpendicular line - opposite side.
        // Opposite = Hypotenuse * sin, where sin = 1 - cos,
        // and cos = dot product of two normalized direction vectors.
        HeadDir = normal(HeadLoc - StartTrace);
        // cos=1.0 - target in front of us
        // cos=0.0 - target is at side (left or right)
        // cos=0.5 - target is 45 degrees from our looking direction
        cosine = LookDir dot HeadDir;
        if ( cosine > 0.9 && VSizeSquared(HeadLoc - StartTrace) * (1.0-cosine) < 25 )
            return true;
    }
    return false;
}



simulated function DisplayCurrentAchievement()
{
    if ( Viewport(Player) == None )
        return;
    //log("DisplayCurrentAchievement:" @CurrentAchievement.ID @ CurrentAchievement.CurrentProgress$"/"$ CurrentAchievement.MaxProgress ,'ScrnBalance');
    if ( CurrentAchHandler != none ) {
        if ( CurrentAchHandler.AchDefs[CurrentAchIndex].bUnlockedJustNow ) {
            //set cooldown only for earned achievements. Status updates can be immediately overrided
            AchievementDisplayCooldown = default.AchievementDisplayCooldown;
            ClientPlaySound(Sound'KF_InterfaceSnd.Perks.PerkAchieved',true,2.f,SLOT_Talk);
            ClientPlaySound(Sound'KF_InterfaceSnd.Perks.PerkAchieved',true,2.f,SLOT_Interface);
        }
        else {
            AchievementDisplayCooldown = fmax(0, AchievementDisplayCooldown);
        }
        CurrentAchHandler.AchDefs[CurrentAchIndex].bUnlockedJustNow = false;
        ReceiveLocalizedMessage(Class'ScrnAchievementEarnedMsg',CurrentAchIndex,self.PlayerReplicationInfo,,CurrentAchHandler);
    }
}


//inform client that he earned an achievement
simulated function DisplayAchievementStatus(ScrnAchievements AchHandler, int AchIndex)
{
    if ( Viewport(Player) == None || AchHandler == none || !AchHandler.IsAchievementIndexValid(AchIndex) )
        return;
    if ( AchHandler.AchDefs[AchIndex].CurrentProgress > 0 ) {
        if ( AchHandler.AchDefs[AchIndex].bUnlockedJustNow )
            ConsoleMessage(Class'ScrnAchievementEarnedMsg'.default.EarnedString $ ": " $ AchHandler.AchDefs[AchIndex].DisplayName, 0, 1, 200, 1);

        if ( PendingAchievements.Length == 0 && AchievementDisplayCooldown <= 0 ) {
            // no need to use a queue for a single achievement
            CurrentAchHandler = AchHandler;
            CurrentAchIndex = AchIndex;
            DisplayCurrentAchievement();
        }
        else if ( AchHandler.AchDefs[AchIndex].bUnlockedJustNow ){
            //don't bother player with status updates, when other achievements have been really earned
            PendingAchievements.insert(PendingAchievements.Length, 1);
            PendingAchievements[PendingAchievements.Length-1].AchHandler = AchHandler;
            PendingAchievements[PendingAchievements.Length-1].AchIndex = AchIndex;
        }
    }
}


static final function string ConsoleColorString(string s, byte R, byte G, byte B)
{
    return chr(27)$chr(max(R,1))$chr(max(G,1))$chr(max(B,1))$s;
}

simulated function ConsoleMessage(string Msg, optional float MsgLife, optional byte R, optional byte G, optional byte B)
{
    if ( Player != none && Player.Console != none ) {
        if ( R != 0 || G != 0 || B != 0 )
            Msg = ConsoleColorString(Msg, R, G, B);
        Player.Console.Message(Msg, MsgLife);
    }
}

simulated function string MyColoredName()
{
    return Mut.ColoredPlayerName(PlayerReplicationInfo);
}

event ClientMessage( coerce string S, optional Name Type )
{
    if ( Level.NetMode == NM_DedicatedServer || GameReplicationInfo == None )
        return;

    if ( Type == 'Log' ) {
        log(S, 'ScrnBalance');
        Type = '';
    }

    if ( Mut != none )
        super.ClientMessage(Mut.ParseColorTags(S, PlayerReplicationInfo), Type);
    else
        super.ClientMessage(S, Type);
}

function LongMessage(string S, optional int MaxLen, optional string Divider)
{
    class'ScrnBalance'.static.LongMessage(self, S, MaxLen, Divider);
}

event TeamMessage( PlayerReplicationInfo PRI, coerce string S, name Type  )
{
    local string c;

    // Wait for player to be up to date with replication when joining a server, before stacking up messages
    if ( Level.NetMode == NM_DedicatedServer || GameReplicationInfo == None )
        return;

    if( AllowTextToSpeech(PRI, Type) )
        TextToSpeech( S, TextToSpeechVoiceVolume );
    if ( Type == 'TeamSayQuiet' )
        Type = 'TeamSay';

    if ( myHUD != None )
        myHUD.Message( PRI, c$S, Type );

        if ( (Player != None) && (Player.Console != None) )
    {
        if ( PRI!=None )
        {
            if ( PRI.Team!=None && GameReplicationInfo.bTeamGame)
            {
                if (PRI.Team.TeamIndex==0)
                    c = chr(27)$chr(200)$chr(1)$chr(1);
                else if (PRI.Team.TeamIndex==1)
                    c = chr(27)$chr(75)$chr(139)$chr(198);
            }
            S = Mut.ColoredPlayerName(PRI)$c$": "$ Mut.ParseColorTags(S, PRI);
        }
        Player.Console.Chat( c$s, 6.0, PRI );
    }
}

simulated function ReceiveLocalizedMessage( class<LocalMessage> Message, optional int Switch, optional PlayerReplicationInfo RelatedPRI_1, optional PlayerReplicationInfo RelatedPRI_2, optional Object OptionalObject )
{
    if ( Message == class'KFVetEarnedMessageSR' ||  Message == class'ScrnPromotedMessage'
            || (Message == class'KFVetEarnedMessagePL' && RelatedPRI_1 == PlayerReplicationInfo) ) {
        Message = class'ScrnPromotedMessage';
    }
    else if ( Message == class'TSCMessages' ) {
        switch (Switch) {
            case 300:     // trying to shop in enemy trader
                ClientCloseMenu(true, true);
                break;
            case 311:   // loosing base means wipe
                if ( ScrnHUD(myHUD) != none )
                    ScrnHUD(myHUD).CriticalOverlayTimer = Level.TimeSeconds + 3.0;
                break;
        }
    }
    super.ReceiveLocalizedMessage(Message, Switch, RelatedPRI_1, RelatedPRI_2, OptionalObject);
}

function ResetWaveStats()
{
    local KFPlayerReplicationInfo KFPRI;

    KFPRI = KFPlayerReplicationInfo(PlayerReplicationInfo);

    bCowboyForWave = true;
    BeggingForMoney = 0;
    bShoppedThisWave = false;
    bHadArmor = bHadArmor || (KFPRI != none    && KFPRI.ClientVeteranSkill != none
        && KFPRI.ClientVeteranSkill.static.ReduceDamage(KFPRI, KFPawn(Pawn), none, 100, none) < 100);

    if ( ScrnHumanPawn(Pawn) != none )
        ScrnHumanPawn(Pawn).ApplyWeaponStats(Pawn.Weapon);
}

exec function LogAch()
{
    local ClientPerkRepLink L;
    local SRCustomProgress S;
    local ScrnAchievements A;
    local int i, c;

    L = class'ScrnClientPerkRepLink'.static.FindMe(self);

    log("#;GROUP;ID;TITLE;DESCRIPTION", 'Achievement');
    log("============================", 'Achievement');
    for( S = L.CustomLink; S != none; S = S.NextLink ) {
        A = ScrnAchievements(S);
        if( A != none ) {
            //log(A.class, 'Package');
            for ( i = 0; i < A.AchDefs.length; ++i ) {
                log(string(++c)
                    $";"$ A.AchDefs[i].Group
                    $";"$ A.AchDefs[i].ID
                    $";"$ A.AchDefs[i].DisplayName
                    $";"$ A.AchDefs[i].Description
                    , 'Achievement');
            }
        }
    }
    ConsoleMessage("Achievement list has been printed to the log file.", 0, 1, 200, 1);
}


exec function AchReset(optional int password)
{
    if ( class'ScrnAchievements'.static.IsAchievementUnlocked(class'ScrnClientPerkRepLink'.static.FindMe(self), 'AchReset') ) {
        if ( AchResetPassword == 0 ) {
            AchResetPassword = 10000 + rand(90000);
            ConsoleMessage("You already have 'AchReset' achievement!", 0, 200, 100, 1);
            ConsoleMessage("If sure you want to do this again, then enter the following command:", 0, 200, 100, 1);
            ConsoleMessage("AchReset " $ AchResetPassword, 0, 200, 200, 1);
            return;
        }
        else if ( AchResetPassword != password ) {
            ConsoleMessage("Wrong reset password!", 0, 200, 100, 1);
            return;
        }
    }
    SrvAchReset();
}

function SrvAchReset()
{
    local ClientPerkRepLink L;
    local array<name> GroupNames;
    local array<string> GroupCaptions;
    local int i;

    L = Class'ScrnClientPerkRepLink'.Static.FindMe(self);
    class'ScrnAchievements'.static.RetrieveGroups(L, GroupNames, GroupCaptions);
    for ( i=0; i<GroupNames.length; ++i ) {
        if ( GroupNames[i] != 'MAP' )
            class'ScrnAchievements'.static.ResetAchievements(L, GroupNames[i]);
    }
    class'ScrnAchievements'.static.ProgressAchievementByID(L, 'AchReset', 1);
}

exec function ResetMyAchievements(name group)
{
    class'ScrnAchievements'.static.ResetAchievements(Class'ScrnClientPerkRepLink'.Static.FindMe(self), group);
}

exec function ResetMapAch(string MapName)
{
    local ClientPerkRepLink L;
    local SRCustomProgress S;
    local ScrnMapAchievements A;
    local int i, j;

    L = Class'ScrnClientPerkRepLink'.Static.FindMe(self);
    if ( MapName == "" )
        MapName = Mut.KF.GetCurrentMapName(Level);

    if ( L == none || MapName == "" )
        return;

    for( S = L.CustomLink; S != none; S = S.NextLink ) {
        A = ScrnMapAchievements(S);
        if( A != none ) {
            for ( i = 0; i < A.AchDefs.length; i += 4 ) {
                if ( String(A.AchDefs[i].ID) ~= MapName ) {
                    for ( j=0; j < 4; ++j ) {
                        A.AchDefs[i+j].bUnlockedJustNow = false;
                        A.AchDefs[i+j].CurrentProgress = 0;
                    }
                    ConsoleMessage("Map achievements locked");
                    return;
                }
            }
        }
    }
}


exec function MVote(string VoteString)
{
    ServerMutate("VOTE " $ VoteString);
}

exec function MVB(string Params)
{
    ServerMutate("VOTE BLAME " $ Params);
}

exec function Say(string Msg)
{
    // Msg = Level.Game.StripColor(Msg);

    if (Msg ~= "!yes")
        ServerMutate("VOTE YES");
    else if (Msg ~= "!no")
        ServerMutate("VOTE NO");
    else
        super.Say(Msg);
}

exec function TeamSay( string Msg )
{
    //Msg = Level.Game.StripColor(Msg);

    if (Msg ~= "!yes")
        ServerMutate("VOTE YES");
    else if (Msg ~= "!no")
        ServerMutate("VOTE NO");
    else
        super.TeamSay(Msg);
}

function ServerSpeech( name Type, int Index, string Callsign )
{
    if ( bSpeechVote && Type == 'ACK' ) {
        if ( index == 0 )
            ServerMutate("VOTE TRYYES");
        else if ( index == 1 )
            ServerMutate("VOTE TRYNO");
    }
    if ( Type == 'SUPPORT' && Index == 2 ) {
        BeggingForMoney++;
    }

    super.ServerSpeech(Type, Index, Callsign);
}



exec function AmIMedic()
{
    if ( ScrnHumanPawn(Pawn) != none )
        ConsoleMessage(String(ScrnHumanPawn(Pawn).IsMedic()));
}

// overrided to call vote for pause in unable to pause directly
function bool SetPause( BOOL bPause )
{
    bFire = 0;
    bAltFire = 0;
    if ( !Level.Game.SetPause(bPause, self) ) {
        ServerMutate("VOTE PAUSE 120");
        return false;
    }
    return true;
}

function BecomeSpectator()
{
    local bool bWasOnlySpectator;

    bWasOnlySpectator = PlayerReplicationInfo.bOnlySpectator;
    super.BecomeSpectator();

    if (Role < ROLE_Authority)
        return;

    if ( PlayerReplicationInfo.bOnlySpectator && !bWasOnlySpectator )
        Mut.GameRules.PlayerLeaving(self);

    if ( Mut.bDynamicLevelCap && Mut.KF.bTradingDoorsOpen )
        Mut.DynamicLevelCap();
}

function BecomeActivePlayer()
{
    local bool bWasOnlySpectator;

    bWasOnlySpectator = PlayerReplicationInfo.bOnlySpectator;
    super.BecomeActivePlayer();


    if (Role < ROLE_Authority) {
        if ( ScrnHUD(MyHUD) != none ) {
            ScrnHUD(MyHUD).DisableHudHacks();
        }
        return;
    }

    if ( !PlayerReplicationInfo.bOnlySpectator ) {
        if ( Mut.KF.bTradingDoorsOpen ) {
            if ( Mut.bDynamicLevelCap )
                Mut.DynamicLevelCap();
        }
        StartCash = PlayerReplicationInfo.Score;
        if ( bWasOnlySpectator )
            Mut.GameRules.PlayerEntering(self);
    }
}

function Possess(Pawn aPawn)
{
    local Rotator R, R2Door;
    local KFTraderDoor door, BestDoor;
    local ShopVolume shop;
    local bool bTurnAroundValid;

    //ClientMessage("Possess("$aPawn$"). Current Pawn="$Pawn);
    if ( Role == ROLE_Authority && Mut != none ) {
        if ( bHadPawn )
            StartCash = max(StartCash, Mut.KF.MinRespawnCash);
        else
            StartCash = max(StartCash, Mut.KF.StartingCash);

        if ( Mut.ScrnGT != none && Mut.ScrnGT.ScrnGameLength != none && Mut.ScrnGT.ScrnGameLength.Wave.bStartAtTrader )
        {
            R = aPawn.Rotation;
            // spawnned at the trader on a trader teleporter.
            // Releporters usually are facing away from the trader, so turn 180
            R.yaw += 32768;
            R = Normalize(R);

            shop = Mut.ScrnGT.TeamShop(PlayerReplicationInfo.Team.TeamIndex);
            foreach aPawn.VisibleCollidingActors(class'KFTraderDoor', door, 1000, aPawn.Location) {
                if ( door.Tag == shop.Event ) {
                    if ( BestDoor == none )
                        BestDoor = door;
                    R2Door = R;
                    R2Door.yaw = rotator(door.Location - aPawn.Location).yaw;
                    if ( abs(Normalize(R2Door - R).yaw) < 4096 ) {
                        // rotation to the door is almost the same as rotation by 180.
                        // In that case use turnaround as it looks better
                        bTurnAroundValid = true;
                    }
                    break;
                }
            }
            if ( !bTurnAroundValid && BestDoor != none ) {
                // turnaround doesn't face any of trader doors. Rotate to door directly
                R.yaw = rotator(door.Location - aPawn.Location).yaw;
            }
            aPawn.SetRotation(R);
        }
    }

    super.Possess(aPawn);

    // show path to trader if respawned during the trader time
    if ( Role == ROLE_Authority && Pawn != none && Mut != none && Mut.KF.bTradingDoorsOpen )
        SetShowPathToTrader(true);

    bHadPawn = bHadPawn || Pawn != none;
}

simulated event Destroyed()
{
    bDestroying = true;

    if ( ScrnHumanPawn(Pawn) != none )
        ScrnHumanPawn(Pawn).HealthBeforeDeath = Pawn.Health;

    if ( Level.NetMode != NM_DedicatedServer && Level.GetLocalPlayerController() == self )
        RemoveVotingMsg(); // delete voting interactions

    if ( Role == ROLE_Authority ) {
        if ( Mut != none &&  PlayerReplicationInfo != none ) {
            if ( Mut.bLeaveCashOnDisconnect && PlayerReplicationInfo.Team != none
                && PlayerReplicationInfo.Score > StartCash )
            {
                PlayerReplicationInfo.Team.Score += PlayerReplicationInfo.Score - StartCash;
                PlayerReplicationInfo.Score = StartCash; // just in case
            }
            if ( Mut.GameRules != none )
                Mut.GameRules.PlayerLeaving(self);
        }
    }

    super.Destroyed();

    if ( Role == ROLE_Authority ) {
        if ( Mut != none && Mut.bDynamicLevelCap && Mut.KF.bTradingDoorsOpen )
            Mut.DynamicLevelCap();
    }
}

simulated function ClientEnterZedTime()
{
    super.ClientEnterZedTime();
    bZEDTimeActive = true;
}

simulated function ClientExitZedTime()
{
    super.ClientExitZedTime();
    bZEDTimeActive = false;
}

function bool AllowVoiceMessage(name MessageType)
{
    local float TimeSinceLastMsg;

    if ( Level.NetMode == NM_Standalone || (PlayerReplicationInfo != none && PlayerReplicationInfo.bAdmin) )
        return true;

    TimeSinceLastMsg = Level.TimeSeconds - OldMessageTime;

    if ( TimeSinceLastMsg < 3 )
    {
        if ( (MessageType == 'TAUNT') || (MessageType == 'AUTOTAUNT') )
            return false;
        if ( TimeSinceLastMsg < 1 )
            return false;
    }

    // zed time screws up voice messages
    if ( !bZEDTimeActive && MessageType != 'TRADER' && MessageType != 'AUTO' ) {
        OldMessageTime = Level.TimeSeconds;
        if ( TimeSinceLastMsg < 10 ) {
            if ( MaxVoiceMsgIn10s > 0 )
                MaxVoiceMsgIn10s--;
            else {
                ClientMessage("Keep quiet for " $ ceil(10-TimeSinceLastMsg) $"s");
                return false;
            }
        }
        else
            MaxVoiceMsgIn10s = default.MaxVoiceMsgIn10s;
    }
    return true;
}

exec function RemoveVotingMsg()
{
    local VHInteraction RemoveMe;

    foreach AllObjects(class'VHInteraction', RemoveMe) {
        RemoveMe.Master.RemoveInteraction(RemoveMe);
    }
}

exec function DropAllWeapons()
{
    ServerDropAllWeapons();
}

function ServerDropAllWeapons()
{
    class'ScrnHumanPawn'.static.DropAllWeapons(Pawn);
}

exec function LockWeapons()
{
    if ( Mut.bAllowWeaponLock ) {
        bWeaponsLocked = true;
        ServerLockWeapons(bWeaponsLocked);
        ClientMessage(ConsoleColorString(strLocked, 192, 1, 1));
    }
    else
        ClientMessage(strLockDisabled);
}

exec function UnlockWeapons()
{
    if ( Mut.bAllowWeaponLock ) {
        bWeaponsLocked = false;
        ServerLockWeapons(bWeaponsLocked);
        ClientMessage(ConsoleColorString(strUnlocked, 1, 192, 1));
    }
    else
        ClientMessage(strLockDisabled);
}

exec function ToggleWeaponLock()
{
    if ( bWeaponsLocked )
        UnlockWeapons();
    else
        LockWeapons();
}

function ServerLockWeapons(bool bLock)
{
    bWeaponsLocked = Mut.bAllowWeaponLock && bLock;
}


exec function Ready()
{
    if ( PlayerReplicationInfo.bOnlySpectator )
        BecomeActivePlayer();

    if ( PlayerReplicationInfo.Team == none || Level.GRI.bMatchHasBegun )
        return;

    if ( Level.NetMode == NM_Standalone || !PlayerReplicationInfo.bReadyToPlay ) {
        SendSelectedVeterancyToServer(true);

        //Set Ready
        ServerRestartPlayer();
        PlayerReplicationInfo.bReadyToPlay = True;
        if ( Level.GRI.bMatchHasBegun )
            ClientCloseMenu(true, false);
    }
}

exec function Spectate()
{
    if ( PlayerReplicationInfo.bOnlySpectator ) {
        ConsoleMessage(strAlreadySpectating);
        return;
    }
    ClientCloseMenu(true, false);
    BecomeSpectator();
}

exec function Unready()
{
    if ( Level.GRI.bMatchHasBegun )
        return;

    ServerUnreadyPlayer();
    PlayerReplicationInfo.bReadyToPlay = False;
}

exec function ShowMenu()
{
    if ( Level.GRI.bMatchHasBegun || PlayerReplicationInfo.bOnlySpectator )
        super.ShowMenu();
    else
        ShowLobbyMenu();
}

function ShowLobbyMenu()
{
    StopForceFeedback();  // jdf - no way to pause feedback

    bPendingLobbyDisplay = false;

    // Open menu
    if (  TSCGameReplicationInfo(Level.GRI) != none && !TSCGameReplicationInfo(Level.GRI).bSingleTeamGame )
        ClientOpenMenu(TSCLobbyMenuClassString);
    else
        ClientOpenMenu(LobbyMenuClassString);
}


function LoadDualWieldables()
{
    local ScrnClientPerkRepLink L;
    local class<KFWeaponPickup> WP;
    local class<KFWeapon> W;
    local int i;

    L = class'ScrnClientPerkRepLink'.static.FindMe(self);
    if( L==None || L.ShopInventory.Length == 0 )
        return; // Hmmmm?


    for ( i=0; i<L.ShopInventory.Length; ++i ) {
        WP = class<KFWeaponPickup>(L.ShopInventory[i].PC);
        if ( WP == none )
            continue;
        W = class<KFWeapon>(WP.default.InventoryType);
        if ( W != none && W.Default.DemoReplacement != none )
            AddDualWieldable(class<KFWeapon>(W.Default.DemoReplacement), W);
    }

    bDualWieldablesLoaded = L.PendingWeapons == 0;
}

final function AddDualWieldable(class<KFWeapon> SingleWeapon, class<KFWeapon> DualWeapon)
{
    local int i;

    if ( SingleWeapon == none || DualWeapon == none )
        return;

    for ( i=0; i<DualWieldables.Length; ++i ) {
        if ( DualWieldables[i].Single == SingleWeapon || DualWieldables[i].Dual == DualWeapon)
            break;
    }
    if ( i == DualWieldables.Length )
        DualWieldables.insert(i, 1);

    DualWieldables[i].Single = SingleWeapon;
    DualWieldables[i].Dual = DualWeapon;
}

function bool IsSinglePistol(class<KFWeapon> Weapon)
{
    local int i;

    if ( !bDualWieldablesLoaded )
        LoadDualWieldables();

    if ( Weapon == none )
        return false;

    for ( i=0; i<DualWieldables.Length; ++i ) {
        if ( DualWieldables[i].Single == Weapon )
            return true;
    }

    return false;
}

function bool AreDualPistols(class<KFWeapon> Weapon)
{
    local int i;

    if ( !bDualWieldablesLoaded )
        LoadDualWieldables();

    if ( Weapon == none )
        return false;

    for ( i=0; i<DualWieldables.Length; ++i ) {
        if ( DualWieldables[i].Dual == Weapon )
            return true;
    }

    return false;
}

// if pickup is in inventory, then always returns true, even if bCheckForEquivalent = false,
// that way fixing picking up of the same weapon  -- PooSH
function bool IsInInventory(class<Pickup> PickupToCheck, bool bCheckForEquivalent, bool bCheckForVariant)
{
    local Inventory CurInv;
    local class<KFWeaponPickup> InvPickupClass;
    local class<KFWeaponPickup> WeaponPickupToCheck;
    local int i;

    if ( PickupToCheck == none )
        return false;

    WeaponPickupToCheck = class<KFWeaponPickup>(PickupToCheck);
    for ( CurInv = Pawn.Inventory; CurInv != none; CurInv = CurInv.Inventory ) {
        if ( /* bCheckForEquivalent && */ CurInv.default.PickupClass == PickupToCheck ) {
            if ( !bCheckForEquivalent && IsSinglePistol(class<KFWeapon>(CurInv.class)) )
                return false; // allow picking up a second pistol
            return true;
        }

        if( !bCheckForVariant )
            continue;

        // check if Item is variant of normal inventory item
        InvPickupClass = class<KFWeaponPickup>(CurInv.default.PickupClass);
        if( InvPickupClass != none ) {
            for( i = 0; i < InvPickupClass.default.VariantClasses.Length; ++i ) {
                if( InvPickupClass.default.VariantClasses[i] == PickupToCheck )
                    return true;
            }
        }
        // check if Item is normal version of variant inventory item
        if( WeaponPickupToCheck != none && CurInv.default.PickupClass != none ) {
            for( i = 0; i < WeaponPickupToCheck.default.VariantClasses.Length; ++i ) {
                if( WeaponPickupToCheck.default.VariantClasses[i] == CurInv.default.PickupClass )
                    return true;
            }
        }
    }

    return false;
}

exec function SetName(coerce string S)
{
    if ( S == "#" )
        S = PlayerName;

    if ( S == "" || (class'ScrnSrvReplInfo'.static.Instance().bForceSteamNames
                    && Player.GUIController.SteamGetUserName() != Mut.StripColorTags(S)) )
    {
        S = Player.GUIController.SteamGetUserName();
        if ( S == "" )
            PlayerName = "";
    }
    else {
        PlayerName = S;
    }
    ChangeName(S);
    UpdateURL("Name", S, true);
    SaveConfig();
}

function ChangeName( coerce string S )
{
    local string PlainName;

    ReplaceText(S, " ", "_");
    ReplaceText(S, "\"", "");

    PlainName = Mut.StripColorTags(S);
    if ( len(PlainName) > 20 )
        S = left(PlainName, 20);

    Level.Game.ChangeName( self, S, true );
}

// TSC stuff

exec function SwitchTeam()
{
    if ( PlayerReplicationInfo == none || PlayerReplicationInfo.Team == None || PlayerReplicationInfo.Team.TeamIndex == 1 )
        ServerChangeTeam(0);
    else
        ServerChangeTeam(1);
}

exec function ChangeTeam( int N )
{
    ServerChangeTeam(N);
}

function ServerChangeTeam( int N )
{
    super.ServerChangeTeam(N);

    if ( Mut.bDynamicLevelCap && Mut.KF.bTradingDoorsOpen )
        Mut.DynamicLevelCap();
}

exec function SetupBase()
{
    ServerScoreFlag();
}

function ServerScoreFlag()
{
    local GameObject Flag;

    Flag = GameObject(PlayerReplicationInfo.HasFlag);
    if ( Flag != none ) {
        if ( Flag.bHeld )
            Flag.Score();
        else
            Flag.ClearHolder(); // shouldn't happen
    }
}

exec function AltFire( optional float F )
{
    if ( Level.Pauser == PlayerReplicationInfo )
    {
        SetPause(false);
        return;
    }
    if( bDemoOwner || (Pawn == None) )
        return;

    if ( !bTSCAdvancedKeyBindings && PlayerReplicationInfo.HasFlag != none
            && GameObject(PlayerReplicationInfo.HasFlag) != none
            && TSCGameReplicationInfo(Level.GRI) != none )
    {
        if ( bTSCAutoDetectKeys )
            bTSCAdvancedKeyBindings = ConsoleCommand("BINDINGTOKEY SetupBase") != "";
        if ( !bTSCAdvancedKeyBindings ) {
            SetupBase();
            return;
        }
    }

    super.AltFire(F);
}

exec function ThrowWeapon()
{
    if ( !bTSCAdvancedKeyBindings && GameObject(PlayerReplicationInfo.HasFlag) != none ) {
        if ( bTSCAutoDetectKeys )
            bTSCAdvancedKeyBindings = ConsoleCommand("BINDINGTOKEY DropFlag") != "";
        if ( !bTSCAdvancedKeyBindings ) {
            DropFlag();
            return;
        }
    }
    ServerThrowWeapon();
}

// character selection
simulated function bool IsTeamCharacter(string CharacterName)
{
    if ( CharacterName == "" )
        return false;

    if ( Level.GRI.bNoTeamSkins || TSCGameReplicationInfo(Level.GRI) == none || PlayerReplicationInfo == none || PlayerReplicationInfo.Team == none )
        return true; // no team = any character can be used

    if ( PlayerReplicationInfo.Team.TeamIndex == 0 )
        return IsRedCharacter(CharacterName);
    else if ( PlayerReplicationInfo.Team.TeamIndex == 1 )
        return IsBlueCharacter(CharacterName);

    return false;
}

simulated function bool IsRedCharacter(string CharacterName)
{
    local int i;

    if ( CharacterName == "" )
        return false;

    for ( i=0; i<RedCharacters.Length; ++i )
        if ( RedCharacters[i] ~= CharacterName )
            return true;

    return false;
}

simulated function bool IsBlueCharacter(string CharacterName)
{
    local int i;

    if ( CharacterName == "" )
        return false;

    for ( i=0; i<BlueCharacters.Length; ++i )
        if ( BlueCharacters[i] ~= CharacterName )
            return true;

    return false;
}

exec function ChangeCharacter(string newCharacter, optional string inClass)
{
    local ClientPerkRepLink L;

    if ( newCharacter == "" || !IsTeamCharacter(newCharacter) )
        return;

    L = class'ScrnClientPerkRepLink'.static.FindMe(self);
    if( L!=None )
        L.SelectedCharacter(newCharacter);
    else
        Super(KFPlayerController_Story).ChangeCharacter(newCharacter,inClass);
}

function SetPawnClass(string inClass, string inCharacter)
{
    ValidateCharacter(inCharacter);
    super.SetPawnClass(inClass, inCharacter);
}

/**
 * Checks if character can be used on this player.
 * @param   CharacterName   [in]    name of the character to validate (xUtil.PlayerRecord.DefaultName)
 *                          [out]   name of the valid character
 * @return  True, if given character can be used. CharacterName stays unchanged.
 *          False,  if given character is invalid. CharacterName changed to valid character.
 */
simulated function bool ValidateCharacter(out string CharacterName)
{
    if ( IsTeamCharacter(CharacterName) )
        return true;

    if ( PlayerReplicationInfo.Team.TeamIndex == 0 ) {
        if ( RedCharacter != "" && IsRedCharacter(RedCharacter) )
            CharacterName = RedCharacter; // only on client side
        else
            CharacterName = RedCharacters[0];
    }
    else if ( PlayerReplicationInfo.Team.TeamIndex == 1 ) {
        if ( BlueCharacter != "" && IsBlueCharacter(BlueCharacter) )
            CharacterName = BlueCharacter; // only on client side
        else
            CharacterName = BlueCharacters[0];
    }
    return false;
}

function SendSelectedVeterancyToServer(optional bool bForceChange)
{
    local class<KFVeterancyTypes> OldPerk;

    if( Level.NetMode!=NM_Client && SRStatsBase(SteamStatsAndAchievements)!=none ) {
        OldPerk = KFPlayerReplicationInfo(PlayerReplicationInfo).ClientVeteranSkill;
        SRStatsBase(SteamStatsAndAchievements).WaveEnded();
        if ( OldPerk != KFPlayerReplicationInfo(PlayerReplicationInfo).ClientVeteranSkill )
            PerkChangeWave = KFGameType(Level.Game).WaveNum;
    }
}

function SelectVeterancy(class<KFVeterancyTypes> VetSkill, optional bool bForceChange)
{
    local ClientPerkRepLink L;

    if( SRStatsBase(SteamStatsAndAchievements)!=none )
        L = Class'ScrnClientPerkRepLink'.Static.FindMe(self);

    if ( L != none ) {
        if ( ScrnClientPerkRepLink(L) != none )
            ScrnClientPerkRepLink(L).ServerSelectPerkSE(Class<ScrnVeterancyTypes>(VetSkill));
        else
            L.ServerSelectPerk(Class<SRVeterancyTypes>(VetSkill));
    }
}

exec final function TourneyCheck()
{
    ServerTourneyCheck();
}

final function ServerTourneyCheck()
{
    local ScrnGameType GT;
    local Mutator M;
    local GameRules G;
    local string s;

    ClientMessage(string(Level.Game.class));

    GT = ScrnGameType(Level.Game);
    if ( GT == none )
        ClientMessage("Current game type doesn't support Tourney Mode");
    else {
        if ( GT.IsTourney() )
            ClientMessage("Tourney Mode: " $ GT.GetTourneyMode());
        else
            ClientMessage("Tourney Mode DISABLED");
    }
    s = "Mutators:";
    for ( M = Level.Game.BaseMutator; M != None; M = M.NextMutator )
        s @= string(M.class);
    LongMessage(s, 80, " ");

    s = "Rules:";
    for ( G=Level.Game.GameRulesModifiers; G!=None; G=G.NextGameRules )
        s @= string(G.class);
    LongMessage(s, 80, " ");
}

exec final function AdminKillMut(string MutName)
{
    if ( Level.NetMode == NM_Standalone || (PlayerReplicationInfo != none && PlayerReplicationInfo.bAdmin) )
        ServerKillMut(MutName);
}

final function ServerKillMut(string MutatorName)
{
    local Mutator M;
    local ScrnBalance SBM;

    if ( Level.NetMode != NM_Standalone && (PlayerReplicationInfo == none || !PlayerReplicationInfo.bAdmin) )
        return;

    if ( MutatorName ~= "KillingFloorMut" || MutatorName ~= "ScrnBalance" || MutatorName ~= "ServerPerksMut" )
        return;

    SBM = class'ScrnBalance'.static.Myself(Level);
    if ( SBM == none || GetItemName(string(SBM.class)) ~= MutatorName
            || GetItemName(string(SBM.FindServerPerksMut().class)) ~= MutatorName )
        return; // can't kill ScrnBalance or ServerPerks

    for ( M = Level.Game.BaseMutator; M != None; M = M.NextMutator ) {

        if ( GetItemName(string(M.class)) ~= MutatorName ) {
            ClientMessage("Mutator '"$string(M.Class)$"' destroyed");
            M.Destroy();
            return;
        }
    }
}

exec final function AdminKillRules(string RuleName)
{
    if ( Level.NetMode == NM_Standalone || (PlayerReplicationInfo != none && PlayerReplicationInfo.bAdmin) )
        ServerKillRules(RuleName);
}

final function ServerKillRules(string RuleName)
{
    local GameRules G, PrevG;

    if ( Level.NetMode != NM_Standalone && (PlayerReplicationInfo == none || !PlayerReplicationInfo.bAdmin) )
        return;

    if ( RuleName ~= "SRGameRules" || RuleName ~= "ScrnGameRules" )
        return; // can't kill ScrnGameRules

    PrevG = Level.Game.GameRulesModifiers;
    if ( GetItemName(string(PrevG.class)) ~= RuleName ) {
        ClientMessage("Game Rules '"$string(PrevG.Class)$"' destroyed");
        Level.Game.GameRulesModifiers = PrevG.NextGameRules;
        PrevG.Destroy();
    }
    else {
        for ( G=PrevG.NextGameRules; G!=None; PrevG=G ) {
            if ( GetItemName(string(G.class)) ~= RuleName ) {
                PrevG.NextGameRules = G.NextGameRules;
                ClientMessage("Game Rules '"$string(G.Class)$"' destroyed");
                G.Destroy();
                return;
            }
        }
    }
}


function CheckDLC()
{
    local ScrnSteamStatsGetter A;

    foreach AllActors(class'ScrnSteamStatsGetter', A)
        return; // do not allow duplicates

    spawn(class'ScrnSteamStatsGetter',self);
}

// returns true if players owns DLC
function bool OwnsDLC(int AppID)
{
    local int i;
    local bool bNeedCheck;

    for ( i=0; i<DLC.length; ++i ) {
        bNeedCheck = bNeedCheck || !DLC[i].bChecked;
        if ( DLC[i].AppID == AppID)
            break;
    }
    if ( i == DLC.length ) {
        DLC.insert(i, 1);
        DLC[i].AppID = AppID;
    }

    if ( bNeedCheck )
        CheckDLC(); // this doesn't happen immediately, so function returns false in the first try

    return DLC[i].bOwnsDLC;
}

/**
 * Sets gun skin according to index in pickup's VariantClasses[]. Check DLC locks.
 *
 * @param index     Skin index:
 *                  0 - switch to next skin
 *                  1 - switch to default skin
 *                  2+  switch to skin defined in Weapon.PickupClass.VariantClasses array.
 *                      2 - first item in array (VariantClasses[0]), 3 - second (VariantClasses[1]) etc.
 * @param bTryNext  If this skin is locked (DLC), then should we try selecting next skin?
 */
exec function GunSkin(optional byte index, optional bool bTryNext)
{
    local KFWeapon W;
    local class<KFWeaponPickup> P;
    local int i;
    local class<KFWeapon> SkinnedWeaponClass;
    local byte k;

    if ( Pawn == none || Pawn.Weapon == none )
        return;
    W = KFWeapon(Pawn.Weapon);
    P = class<KFWeaponPickup>(Pawn.Weapon.PickupClass);
    if ( W == none || P == none || P.default.VariantClasses.Length == 0 )
        return;

    if ( W.SleeveNum == 0 )
        k = 1; // for weapons where hand skin is first in array

    if ( k >= W.Skins.length ) {
        warn(W.class $ " has no skins");
        return;
    }

    if ( index == 0 ) {
        bTryNext = true;
        // find out current skin
        if ( W.Skins[k] == W.default.Skins[k] )
            index = 1; // default skin
        else {
            for ( i=0; i<P.default.VariantClasses.Length; ++i ) {

                if ( P.default.VariantClasses[i] == none ) {
                    //bugfix
                    P.default.VariantClasses.remove(i--,1);
                    continue;
                }

                if ( W.Skins[k] == P.default.VariantClasses[i].default.InventoryType.default.Skins[k] )
                {
                    index = i + 2;
                    break;
                }
            }
        }
        if ( P.default.VariantClasses.Length == 0 )
            return; // bugfix, when VariantClasses have "none" elements
        index++;
    }

    if ( index >= P.default.VariantClasses.Length+2 )
        index = 1;

    if ( index == 1 ) {
        // load default skin
        SkinnedWeaponClass = W.class;
    }
    else {
        // load skin from variant class
        SkinnedWeaponClass = class<KFWeapon>(P.default.VariantClasses[index-2].default.InventoryType);
        // DLC check
        if ( SkinnedWeaponClass == none || (!Mut.bBeta && SkinnedWeaponClass.default.AppID > 0
                && !OwnsDLC(SkinnedWeaponClass.default.AppID)) )
        {
            // this skin is locked
            if ( bTryNext )
                GunSkin(index+1, true); // try next one
            return;
        }
    }

    if ( SkinnedWeaponClass == none )
        return;
    ClientWeaponSpawned(SkinnedWeaponClass, none); // preload assets
    SkinnedWeaponClass.static.PreloadAssets(W);
    W.HandleSleeveSwapping(); // restore proper arms
    ServerGunSkin(SkinnedWeaponClass);

    // save skin in user.ini
    for ( i=0; i<WeaponSettings.length; ++i )
        if ( W.class == WeaponSettings[i].Weapon )
            break;
    if ( i == WeaponSettings.length ) {
        WeaponSettings.insert(i,1);
        WeaponSettings[i].Weapon = W.class;
    }
    if ( WeaponSettings[i].SkinIndex != index ) {
        WeaponSettings[i].SkinIndex = index;
        WeaponSettings[i].LastWeapon = W;
        SaveConfig();
    }
}

// replicate attachment (3-rd person model) skin to other players
function ServerGunSkin(class<KFWeapon> SkinnedWeaponClass)
{
    local Controller C;
    local int i;
    local KFWeapon W;

    // preload assets
    for ( C = Level.ControllerList; C != none; C = C.nextController ) {
        if ( C != self && KFPlayerController(C) != none )
            KFPlayerController(C).ClientWeaponSpawned(SkinnedWeaponClass, none);
    }

    if ( Pawn != none )
        W = KFWeapon(Pawn.Weapon);
    if ( W != none && W.AttachmentClass != SkinnedWeaponClass.default.AttachmentClass ) {
        W.ThirdPersonActor.Destroy();
        W.ThirdPersonActor = none;
        W.AttachmentClass = SkinnedWeaponClass.default.AttachmentClass;
        W.AttachToPawn(Pawn);

        //remember weapon choise
        for ( i=0; i<WeaponSettings.length; ++i )
            if ( W.class == WeaponSettings[i].Weapon )
                break;
        if ( i == WeaponSettings.length ) {
            WeaponSettings.insert(i,1);
            WeaponSettings[i].Weapon = W.class;
        }
        WeaponSettings[i].SkinnedWeapon = SkinnedWeaponClass;
    }
}

function LoadGunSkinFromConfig()
{
    local int i;

    if ( Pawn == none || Pawn.Weapon == none )
        return;

    for ( i=0; i<WeaponSettings.length; ++i ) {
        if ( Pawn.Weapon.class == WeaponSettings[i].Weapon ) {
            if ( WeaponSettings[i].LastWeapon != Pawn.Weapon )
                GunSkin(WeaponSettings[i].SkinIndex);
            break;
        }
    }
}

// overrided KFWeapon behavior to block reloading while putting down, bringing up or throwing the nade
exec function ReloadMeNow()
{
    local KFWeapon W;

    if ( Pawn != none )
        W = KFWeapon(Pawn.Weapon);
    if ( W == none )
        return;

    // ClientMessage( "Reloading Weapon: "
        // @ "Timer=" $ W.TimerCounter$"/"$W.TimerRate
        // @ "ClientState=" $ GetEnum(Enum'EWeaponClientState', W.ClientState)
        // @ "ClientGrenadeState=" $ GetEnum(Enum'EClientGrenadeState', W.ClientGrenadeState)
    // );

    if ( W.ClientState == WS_ReadyToFire /* && W.ClientGrenadeState == GN_None */ )
        W.ReloadMeNow();
}

exec function GetWeapon(class<Weapon> NewWeaponClass )
{
    if ( ScrnHumanPawn(Pawn) != none && ScrnHumanPawn(Pawn).bQuickMeleeInProgress )
        return;
    super.GetWeapon(NewWeaponClass);
}

exec function NextWeapon()
{
    if ( ScrnHumanPawn(Pawn) != none && ScrnHumanPawn(Pawn).bQuickMeleeInProgress )
        return;
    super.NextWeapon();
}

exec function PrevWeapon()
{
    if ( ScrnHumanPawn(Pawn) != none && ScrnHumanPawn(Pawn).bQuickMeleeInProgress )
        return;
    super.PrevWeapon();
}

// STATES

state Dead
{
    // fix of null-reference
    function Timer()
    {
        if ( KFStoryGameInfo(Level.Game) != none )
            super.Timer();
        else
            super(KFPlayerController).Timer(); // bypass KFPlayerController_Story
    }
}

    // view next player
function ServerViewNextPlayer()
{
    local Controller C, Pick;
    local bool bFound, bWasSpec;

    if( !IsInState('Spectating') )
        return;

    if ( ScrnGameType(Level.Game) == none ) {
        super.ServerViewNextPlayer();
        ViewTargetChanged();;
        return;
    }

    // copy-pasted to remove team hack
    bWasSpec = !bBehindView && (ViewTarget != Pawn) && (ViewTarget != self);
    for ( C=Level.ControllerList; C!=None; C=C.NextController ) {
        if ( Level.Game.CanSpectate(self,PlayerReplicationInfo.bOnlySpectator,C) ) {
            if ( Pick == None )
                Pick = C;
            if ( bFound ) {
                Pick = C;
                break;
            }
            else
                bFound = RealViewTarget == C || ViewTarget == C;
        }
    }
    if ( Pick == none )
        Pick = self;
    SetViewTarget(Pick);
    ClientSetViewTarget(Pick);
    if ( (ViewTarget == self) || bWasSpec )
        bBehindView = false;
    else
        bBehindView = true; //bChaseCam;
    ClientSetBehindView(bBehindView);
    ViewTargetChanged();
}

function ServerViewSelf()
{
    local Actor A;
    local vector Loc;
    local rotator R;

    if ( !PlayerReplicationInfo.bOnlySpectator && ScrnGameType(Level.Game) != none
            && ScrnGameType(Level.Game).IsTourney() )
    {
        // free roaming is prohibited in tourney mode
        ServerViewNextPlayer();
        return;
    }

    if ( bBehindView ) {
        PlayerCalcView(A, Loc, R);
    }
    else {
        Loc = ViewTarget.Location;
        R = ViewTarget.Rotation;
    }
    SetLocation(Loc);
    SetRotation(R);
    ClientSetLocation(Loc, R);

    bBehindView = false;
    SetViewTarget(self);
    ClientSetViewTarget(self);
    ClientMessage(OwnCamera, 'Event');
    ViewTargetChanged();
}


// Modes:
// 1 - switch to next team
// 2 - switch to flag carrier
// 3 - cycle through the team
// 4 - assign favorite player
// 5 - cycle between 2 favorite players
// 6 - cycle between big zeds (base HP >= 1000)
// 7 - cycle between Fleshpounds and Patriarch
function ServerSwitchViewMode(byte Mode)
{
    local Controller C, Pick, CurrentTarget;
    local bool bFound;
    local TeamInfo ViewingTeam;

    if( !IsInState('Spectating') || !PlayerReplicationInfo.bOnlySpectator )
        return;

    if ( ScrnGameType(Level.Game) == none )
        return;

    CurrentTarget = Controller(ViewTarget);
    if ( CurrentTarget == none ) {
        if ( Pawn(ViewTarget) != none )
            CurrentTarget = Pawn(ViewTarget).Controller;
        else
            CurrentTarget = RealViewTarget;
    }

    if ( CurrentTarget != none && CurrentTarget.PlayerReplicationInfo != none )
        ViewingTeam = CurrentTarget.PlayerReplicationInfo.Team;


    if ( Mode == 4 ) {
        if ( CurrentTarget == none )
            return;

        FavoriteSpecs[1] = FavoriteSpecs[0];
        FavoriteSpecs[0] = CurrentTarget;
        ClientMessage(strSpecFavoriteAssigned);
        return;
    }
    else if ( Mode == 5 ) {
        if ( FavoriteSpecs[0] == none && FavoriteSpecs[1] == none ) {
            ClientMessage(strSpecNoFavorites);
            return;
        }

        if ( FavoriteSpecs[0] == CurrentTarget )
            Pick = FavoriteSpecs[1];
        else
            Pick = FavoriteSpecs[0];
    }
    else if ( Mode == 6 || Mode == 7 ) {
        // view next Fleshpound / Patriarch
        for ( C=Level.ControllerList; C!=None; C=C.NextController ) {
            if ( C.Pawn == none || C.Pawn.Health <= 0 || C.Pawn.default.Health < 1000 || KFMonsterController(C) == none )
                continue;

            if ( Mode == 7 && FleshpoundZombieController(C) == none && BossZombieController(C) == none )
                continue;

            if ( CurrentTarget == C )
                bFound = true;
            else if ( Pick == None || bFound ) {
                Pick = C;
                if ( bFound )
                    break;
            }
        }
    }
    else {
        // view next player
        for ( C=Level.ControllerList; C!=None; C=C.NextController ) {
            if ( C.PlayerReplicationInfo == none )
                continue;

            if ( Level.Game.CanSpectate(self,true,C) ) {
                if ( CurrentTarget == C )
                    bFound = true;
                else if ( Pick == None || bFound ) {
                    if ( (Mode == 1 && ViewingTeam != C.PlayerReplicationInfo.Team)
                            || (Mode == 2 && C.PlayerReplicationInfo.HasFlag != none)
                            || (Mode == 3 && ViewingTeam == C.PlayerReplicationInfo.Team))
                    {
                        Pick = C;
                        if ( bFound || Mode == 1 )
                            break;
                    }
                }

            }
        }
    }

    if ( Pick == none )
        return;

    ServerSetViewTarget(Pick);
}

function ServerSetViewTarget(Actor NewViewTarget)
{
    local bool bWasSpec;

    if ( !IsInState('Spectating') || (!PlayerReplicationInfo.bOnlySpectator && Mut.SrvTourneyMode > 0) )
        return;

    bWasSpec = !bBehindView && ViewTarget != Pawn && ViewTarget != self;
    SetViewTarget(NewViewTarget);
    ViewTargetChanged();
    ClientSetViewTarget(NewViewTarget);
    if ( ViewTarget == self || bWasSpec )
        bBehindView = false;
    else
        bBehindView = true; //bChaseCam;
    ClientSetBehindView(bBehindView);
}

function ViewTargetChanged()
{
    local Controller C;
    local ScrnHumanPawn ScrnVT;

    //log("ViewTargetChanged("$OldViewTarget$")", 'ScrnBalance');

    if ( Role < ROLE_Authority || (!PlayerReplicationInfo.bOnlySpectator && Mut.SrvTourneyMode > 0) )
        return;

    ScrnVT = ScrnHumanPawn(OldViewTarget);
    if ( ScrnVT != none && ScrnVT != ViewTarget ) {
        // check if somebody else is spectating our old target
        for ( C=Level.ControllerList; C!=None; C=C.NextController ) {
            if ( C.Pawn != ScrnVT && PlayerController(C) != none && PlayerController(C).ViewTarget == ScrnVT )
                break;
        }
        ScrnVT.bViewTarget = C!=none;
    }

    ScrnVT = ScrnHumanPawn(ViewTarget);
    if ( ScrnVT != none )
            ScrnVT.bViewTarget = true; // tell pawn that we are watching him

    OldViewTarget = ViewTarget;
}

function ClientSetBehindView(bool B)
{
    super.ClientSetBehindView(B);

    if ( PlayerReplicationInfo != none && PlayerReplicationInfo.bOnlySpectator ) {
        // auto show crosshair in first person spectator mode
        if ( ScrnHUD(MyHUD) != none )
            ScrnHUD(MyHUD).DebugCrosshair(!bBehindView);
    }
    else if ( B == false ) {
        bFreeCamera = false;
    }
}

exec function FreeCamera( bool B )
{
    ClientMessage("FreeCamera is blocked due to exploits. Use ToggleBehindView instead");
    // free roaming is prohibited in tourney mode
    // if ( B && !PlayerReplicationInfo.bOnlySpectator && Mut.SrvTourneyMode != 0 )
        // return;

    // super.FreeCamera(B);
}


state Spectating
{
   // Return to spectator's own camera.
    exec function AltFire( optional float F )
    {
        // free roaming is prohibited in tourney mode
        if ( !PlayerReplicationInfo.bOnlySpectator && Mut.SrvTourneyMode != 0 )
            Fire(F);
        else
            super.AltFire(F);
    }

    exec function SwitchWeapon(byte T)
    {
        if ( PlayerReplicationInfo.bOnlySpectator )
            ServerSwitchViewMode(T);
    }

    exec function Use()
    {
        local vector HitLocation, HitNormal, TraceEnd, TraceStart;
        local rotator R;
        local Actor A;

        PlayerCalcView(A, TraceStart, R);
        TraceEnd = TraceStart + 1000 * Vector(R);
        A = Trace(HitLocation, HitNormal, TraceEnd, TraceStart, true);
        if ( Pawn(A) != none)
            ServerSetViewTarget(A);
    }
}


// ============================== DEBUG STUFF ==============================

// ========================== UNHARMFUL FUNCTIONS ==========================
exec function TestColorTags(coerce string ColorTagString, optional int i)
{
    local string c, s;

    c = class'ScrnBalance'.default.Mut.ParseColorTags(ColorTagString, PlayerReplicationInfo);
    s = class'ScrnBalance'.default.Mut.StripColorTags(ColorTagString);
    if ( i > 0) {
        c = class'ScrnBalance'.static.LeftCol(c, i);
        s = left(s, i);
    }
    ConsoleMessage(c);
    ConsoleMessage(s);
}


exec function MyTeam()
{
    if ( PlayerReplicationInfo == none )
        ClientMessage("No PlayerReplicationInfo received yet");
    else if ( PlayerReplicationInfo.Team == none )
        ClientMessage("Team is not selected");
    else
        ClientMessage("My Team Index = " $ PlayerReplicationInfo.Team.TeamIndex
            $ " ("$ PlayerReplicationInfo.Team.class$")");
}



function DebugRepLink(string S)
{
    local ClientPerkRepLink L;
    local ScrnClientPerkRepLink SL;
    local int count;

    if ( Role < ROLE_Authority ) {
        foreach DynamicActors(Class'ClientPerkRepLink',L) {
            ++count;
        }
        if ( count != 1 ) {
            if ( count == 0 )
                ClientMessage(S @ "WARNING! No RepLink objects found!", 'Log');
            else
                ClientMessage(S @ "WARNING! Multiple RepLink objects found! ("$count$")", 'Log');
        }
        L = Class'ScrnClientPerkRepLink'.Static.FindMe(self);
        ClientMessage(S @ "RepLink: " $ L, 'Log');
    }
    else {
        L = SRStatsBase(SteamStatsAndAchievements).Rep;
        ClientMessage(S @ "Stats: " $ SteamStatsAndAchievements, 'Log');
        ClientMessage(S @ "Stats.Rep: " $ L, 'Log');
    }

    ClientMessage(S @ "Categories ACK/TOTAL: " $ L.ClientAccknowledged[1]$"/"$L.ShopCategories.Length, 'Log');
    ClientMessage(S @ "Weapons    ACK/TOTAL: " $ L.ClientAccknowledged[0]$"/"$L.ShopInventory.Length, 'Log');
    ClientMessage(S @ "Characters ACK/TOTAL: " $ L.ClientAckSkinNum$"/"$L.CustomChars.Length, 'Log');

    SL = ScrnClientPerkRepLink(L);
    if ( SL != none ) {
        ClientMessage(S @ "Job: " $ SL.CurrentJob, 'Log');
    }
}

function ServerDebugRepLink()
{
    if ( Role == ROLE_Authority )
        DebugRepLink("Server");
}

exec function TestRepLink()
{
    DebugRepLink("Client");
    if ( Role < ROLE_Authority )
        ServerDebugRepLink();
}

exec function RestartRepLink()
{
    local ScrnClientPerkRepLink L;

    if ( KFGameReplicationInfo(Level.GRI).EndGameType > 0 ) {
        ClientMessage("Unable to restart replication when game ended.");
        return;
    }

    L = Class'ScrnClientPerkRepLink'.Static.FindMe(self);
    if ( L == none ) {
        ClientMessage("ScrnClientPerkRepLink not found");
        return;
    }
    bDebugRepLink = true;
    L.StartClientInitialReplication();
}

exec function RepLinkMessages(bool value)
{
    bDebugRepLink = value;
    Class'ScrnClientPerkRepLink'.Static.FindMe(self).bClientDebug = bDebugRepLink;
}

exec function TestQuickMelee()
{
    local Inventory inv;
    local KFWeapon W;
    local string s;
    local int c;

    if ( Pawn == none )
        return;

    for ( inv = Pawn.Inventory; inv != none && ++c < 1000 ; inv = inv.Inventory ) {
        W = KFWeapon(inv);
        if ( W != none ) {
            if ( W == Pawn.Weapon )
                s = "[CURRENT]";
            else
                s = "";
            s @= W.ItemName $ ": ClientState=" $ GetEnum(Enum'EWeaponClientState', W.ClientState);
            s @= "ClientGrenadeState=" $ GetEnum(Enum'EClientGrenadeState', W.ClientGrenadeState);
            ClientMessage(s, 'log');
        }
    }
    ClientMessage("SecondaryItem: " $ ScrnHumanPawn(Pawn).SecondaryItem, 'log');

}

exec function FixQuickMelee()
{
    local Inventory inv;
    local KFWeapon W;
    local int c;

    if ( Pawn == none )
        return;

    if ( Level.NetMode == NM_Client ) {
        ServerFixQuickMelee();  // do the same on server
    }

    KFPawn(Pawn).SecondaryItem = none;
    KFPawn(Pawn).bThrowingNade = false;

    for ( inv = Pawn.Inventory; inv != none && ++c < 1000 ; inv = inv.Inventory ) {
        W = KFWeapon(inv);
        if ( W != none ) {
            if ( W != Pawn.Weapon ) {
                W.ClientState = WS_Hidden;
                W.ClientGrenadeState = GN_None;
            }
            W.SetTimer(0, false);
            W.GotoState('');
        }
    }

    if ( Pawn.Weapon != none && Pawn.Weapon.ClientState != WS_ReadyToFire ) {
        Pawn.Weapon.ClientState = WS_Hidden;
        KFWeapon(Pawn.Weapon).ClientGrenadeState = GN_BringUp;
        Pawn.Weapon.BringUp();
    }
}

function ServerFixQuickMelee()
{
    if ( Level.NetMode != NM_Client ) {
        FixQuickMelee();
    }
}

// ======================== COMMENT BEFORE RELEASE !!! =====================

// exec function SetSID64(coerce string SteamID64)
// {
//     //if ( Level.NetMode == NM_Standalone )
//     if ( Role == ROLE_Authority )
//         class'ScrnCustomPRI'.static.FindMe(PlayerReplicationInfo).SetSteamID64(SteamID64);
// }
//
// exec function TestSID()
// {
//     SetSID64("76561197992537591");
// }
//
// exec function PerkLevel(int L)
// {
//     if ( Role == ROLE_Authority )
//         KFPlayerReplicationInfo(PlayerReplicationInfo).ClientVeteranSkillLevel = L;
// }
//
// exec function TestZedTime(optional float DesiredZedTimeDuration)
// {
//     if ( Role == ROLE_Authority )
//     KFGameType(Level.Game).DramaticEvent(1.0, DesiredZedTimeDuration);
// }
//
// exec function IncAch(name AchID)
// {
//     if ( Level.NetMode == NM_Standalone )
//         class'ScrnAchievements'.static.ProgressAchievementByID(Class'ScrnClientPerkRepLink'.Static.FindMe(self), AchID, 1);
// }
//
// exec function GiveAch(name AchID)
// {
//     if ( Level.NetMode == NM_Standalone )
//         class'ScrnAchievements'.static.ProgressAchievementByID(Class'ScrnClientPerkRepLink'.Static.FindMe(self), AchID, 1000);
// }
//
// exec function BuyAll()
// {
//     local int i;
//     local ScrnClientPerkRepLink L;
//     local ScrnHumanPawn ScrnPawn;
//
//     if ( Role != ROLE_Authority )
//         return;
//
//     PlayerReplicationInfo.Score = 1000000;
//     ScrnPawn = ScrnHumanPawn(Pawn);
//     L = class'ScrnClientPerkRepLink'.static.FindMe(self);
//     for ( i=0; i<L.ShopInventory.length; ++i ) {
//         ScrnPawn.DropAllWeapons(ScrnPawn);
//         ScrnPawn.ServerBuyWeapon(class<Weapon>(L.ShopInventory[i].PC.default.InventoryType), 0);
//     }
// }
//
//
// exec function GiveSpawnInv()
// {
//     if ( Role != ROLE_Authority )
//         return;
//
//     KFPlayerReplicationInfo(PlayerReplicationInfo).ClientVeteranSkill.static.AddDefaultInventory(KFPlayerReplicationInfo(PlayerReplicationInfo), Pawn);
// }
//
// exec function TestEndGame()
// {
//     if ( Role != ROLE_Authority )
//         return;
//
//     Mut.GameRules.bSuperPat = true;
//     Mut.GameRules.bHasCustomZeds = true;
//     Mut.GameRules.GameDoom3Kills = 100;
//     Mut.GameRules.HardcoreLevel = 15;
//     Mut.KF.WaveNum = Mut.KF.FinalWave + 1;
//     Mut.KF.CheckEndGame(PlayerReplicationInfo, "Test");
// }

defaultproperties
{
     bManualReload=True
     bOtherPlayerLasersBlue=True
     bAlwaysDisplayAchProgression=True
     Custom3DScopeSens=24
     bSpeechVote=True
     AchievementDisplayCooldown=5.000000
     bChangedPerkDuringGame=True
     MaxVoiceMsgIn10s=5
     strLocked="Weapon pickups LOCKED"
     strUnlocked="Weapon pickups UNLOCKED"
     strLockDisabled="Weapon lock is disabled by server"
     strAlreadySpectating="Already spectating. Type READY, if you want to join the game."
     DualWieldables(0)=(Single=Class'KFMod.Single',Dual=Class'KFMod.Dualies')
     DualWieldables(1)=(Single=Class'KFMod.Deagle',Dual=Class'KFMod.DualDeagle')
     DualWieldables(2)=(Single=Class'KFMod.GoldenDeagle',Dual=Class'KFMod.GoldenDualDeagle')
     DualWieldables(3)=(Single=Class'KFMod.Magnum44Pistol',Dual=Class'KFMod.Dual44Magnum')
     DualWieldables(4)=(Single=Class'KFMod.MK23Pistol',Dual=Class'KFMod.DualMK23Pistol')
     DualWieldables(5)=(Single=Class'KFMod.FlareRevolver',Dual=Class'KFMod.DualFlareRevolver')
     ProfilePageClassString="ScrnBalanceSrv.ScrnProfilePage"
     TSCLobbyMenuClassString="ScrnBalanceSrv.TSCLobbyMenu"
     bTSCAutoDetectKeys=True
     RedCharacter="Pyro_Red"
     BlueCharacter="Pyro_Blue"
     RedCharacters(0)="Pyro_Red"
     RedCharacters(1)="Agent_Wilkes"
     RedCharacters(2)="Ash_Harding"
     RedCharacters(3)="Chopper_Harris"
     RedCharacters(4)="Corporal_Lewis"
     RedCharacters(5)="Dave_The_Butcher_Roberts"
     RedCharacters(6)="DJ_Scully"
     RedCharacters(7)="Dr_Gary_Glover"
     RedCharacters(8)="FoundryWorker_Aldridge"
     RedCharacters(9)="Harold_Hunt"
     RedCharacters(10)="Harold_Lott"
     RedCharacters(11)="Kerry_Fitzpatrick"
     RedCharacters(12)="Kevo_Chav"
     RedCharacters(13)="KF_Soviet"
     RedCharacters(14)="Lieutenant_Masterson"
     RedCharacters(15)="Mrs_Foster"
     RedCharacters(16)="Mr_Foster"
     RedCharacters(17)="Paramedic_Alfred_Anderson"
     RedCharacters(18)="Police_Constable_Briar"
     RedCharacters(19)="Police_Sergeant_Davin"
     RedCharacters(20)="Private_Schnieder"
     RedCharacters(21)="Reverend_Alberts"
     RedCharacters(22)="Ricky_Vegas"
     RedCharacters(23)="Samuel_Avalon"
     RedCharacters(24)="Security_Officer_Thorne"
     RedCharacters(25)="Sergeant_Powers"
     RedCharacters(26)="Shadow_Ferret"
     RedCharacters(27)="Trooper_Clive_Jenkins"
     RedCharacters(28)="Mr_Magma"
     RedCharacters(29)="Ms_Clamley"
     BlueCharacters(0)="Pyro_Blue"
     BlueCharacters(1)="Baddest_Santa"
     BlueCharacters(2)="Captian_Wiggins"
     BlueCharacters(3)="ChickenNator"
     BlueCharacters(4)="Commando_Chicken"
     BlueCharacters(5)="Dr_Jeffrey_Tamm"
     BlueCharacters(6)="DAR"
     BlueCharacters(7)="Harchier_Spebbington"
     BlueCharacters(8)="Harchier_Spebbington_II"
     BlueCharacters(9)="Hayato_Tanaka"
     BlueCharacters(10)="KF_German"
     BlueCharacters(11)="LanceCorporal_Lee_Baron"
     BlueCharacters(12)="Mike_Noble"
     BlueCharacters(13)="Reaper"
     BlueCharacters(14)="Reggie"
     BlueCharacters(15)="Steampunk_Berserker"
     BlueCharacters(16)="Steampunk_Commando"
     BlueCharacters(17)="Steampunk_Demolition"
     BlueCharacters(18)="Steampunk_DJ_Scully"
     BlueCharacters(19)="Steampunk_Firebug"
     BlueCharacters(20)="Steampunk_Medic"
     BlueCharacters(21)="Steampunk_MrFoster"
     BlueCharacters(22)="Steampunk_Mrs_Foster"
     BlueCharacters(23)="Steampunk_Sharpshooter"
     BlueCharacters(24)="Steampunk_Support_Specialist"
     BlueCharacters(25)="Voltage"
     CustomPlayerReplicationInfoClass=Class'ScrnBalanceSrv.ScrnCustomPRI'
     bDamageAck=True
     strNoPerkChanges="Mid-game perk changes disabled"
     strPerkLocked="Perk is locked"
     strSpecFavoriteAssigned="View target marked as favorite"
     strSpecNoFavorites="No favorite view targets! Press '4' to assign favorite."
     DLC(0)=(AppID=210934)
     DLC(1)=(AppID=210938)
     DLC(2)=(AppID=210939)
     DLC(3)=(AppID=210942)
     DLC(4)=(AppID=210943)
     DLC(5)=(AppID=210944)
     DLC(6)=(AppID=258751)
     DLC(7)=(AppID=258752)
     DLC(8)=(AppID=309991)
     MusicPlaylistNames(0)="<DEFAULT>"
     MusicPlaylistNames(1)="KF1 Classic Soundtrack"
     MusicPlaylistNames(2)="DooM Metal Soundtrack"
     MyMusic(0)=(PL=1,bTrader=True,Song="KF_Defection",Artist="zYnthetic",Title="Defection")
     MyMusic(1)=(PL=1,bTrader=True,Song="KF_Harm",Artist="zYnthetic",Title="Harm Intended")
     MyMusic(2)=(PL=1,bTrader=True,Song="KF_Insect",Artist="zYnthetic",Title="Insect Wings")
     MyMusic(3)=(PL=1,bTrader=True,Song="KF_Mutagen",Artist="zYnthetic",Title="Mutagen")
     MyMusic(4)=(PL=1,bTrader=True,Song="KF_Neurotoxin",Artist="zYnthetic",Title="Neurotoxin")
     MyMusic(5)=(PL=1,bTrader=True,Song="KF_Peripheral",Artist="zYnthetic",Title="Peripheral")
     MyMusic(6)=(PL=1,bTrader=True,Song="KF_SinSoma",Artist="zYnthetic",Title="Sin Soma and the Masquerade")
     MyMusic(7)=(PL=1,bTrader=True,Song="KF_Smolder",Artist="zYnthetic",Title="Smolder")
     MyMusic(8)=(PL=1,bTrader=True,Song="KF_SurfaceTension",Artist="zYnthetic",Title="Surface Tension")
     MyMusic(9)=(PL=1,bTrader=True,Song="KF_TheEdge",Artist="zYnthetic",Title="The Edge Of The Abyss")
     MyMusic(10)=(PL=1,bTrader=True,Song="KF_TheStitches",Artist="zYnthetic",Title="The Stitches Are A Reminder")
     MyMusic(11)=(PL=1,bTrader=True,Song="KF_Treatments",Artist="zYnthetic",Title="Treatments Are More Profitable Than Cures")
     MyMusic(12)=(PL=1,bTrader=True,Song="KF_Vapour",Artist="zYnthetic",Title="Vapour")
     MyMusic(13)=(PL=1,bTrader=True,Song="KF_Wading",Artist="zYnthetic",Title="Wading Through The Bodies")
     MyMusic(14)=(PL=1,Song="DirgeDefective1",Artist="Dirge",Title="Defective")
     MyMusic(15)=(PL=1,Song="DirgeDefective2",Artist="Dirge",Title="Defective")
     MyMusic(16)=(PL=1,Song="DirgeDisunion1",Artist="Dirge",Title="Disunion")
     MyMusic(17)=(PL=1,Song="DirgeDisunion2",Artist="Dirge",Title="Disunion")
     MyMusic(18)=(PL=1,Song="DirgeRepulse1",Artist="Dirge",Title="Repulse")
     MyMusic(19)=(PL=1,Song="DirgeRepulse2",Artist="Dirge",Title="Repulse")
     MyMusic(20)=(PL=1,Song="KF_Infectious_Cadaver",Artist="Six Feet of Foreplay",Title="Infectious Cadaver")
     MyMusic(21)=(PL=1,Song="KF_BledDry",Artist="zYnthetic",Title="Bled Dry")
     MyMusic(22)=(PL=1,Song="KF_Containment",Artist="zYnthetic",Title="Containment Breach")
     MyMusic(23)=(PL=1,Song="KF_Hunger",Artist="zYnthetic",Title="Hunger")
     MyMusic(24)=(PL=1,Song="KF_Pathogen",Artist="zYnthetic",Title="Pathogen")
     MyMusic(25)=(PL=1,Song="KF_WPrevention",Artist="zYnthetic",Title="Witness Prevention")
     MyMusic(26)=(PL=1,Wave=11,Song="KF_Abandon",Artist="zYnthetic",Title="Abandon All")
     MyMusic(27)=(PL=2,bTrader=True,Song="EGT-ThisLove",Artist="elguitarTom",Title="Waiting For Romero To Play")
     MyMusic(28)=(PL=2,bTrader=True,Song="EGT-Interlevel",Artist="elguitarTom",Title="Interlevel")
     MyMusic(29)=(PL=2,Wave=1,Song="EGT-Entryway",Artist="elguitarTom",Title="Entryway")
     MyMusic(30)=(PL=2,Wave=2,Song="EGT-E1M1",Artist="elguitarTom",Title="E1M1")
     MyMusic(31)=(PL=2,Wave=3,Song="EGT-Demon1",Artist="elguitarTom",Title="The Demon's Dead, Part 1")
     MyMusic(32)=(PL=2,Wave=4,Song="EGT-Shawn",Artist="elguitarTom",Title="Shawn's Got The Shotgun")
     MyMusic(33)=(PL=2,Wave=5,Song="EGT-ToxinRefinery",Artist="elguitarTom",Title="Toxin Refinery")
     MyMusic(34)=(PL=2,Wave=6,Song="EGT-Entryway",Artist="elguitarTom",Title="Entryway")
     MyMusic(35)=(PL=2,Wave=7,Song="EGT-Demon2",Artist="elguitarTom",Title="The Demon's Dead, Part 2")
     MyMusic(36)=(PL=2,Wave=8,Song="EGT-PhobosLab",Artist="elguitarTom",Title="Phobos Lab")
     MyMusic(37)=(PL=2,Wave=9,Song="EGT-E1M1",Artist="elguitarTom",Title="E1M1")
     MyMusic(38)=(PL=2,Wave=10,Song="EGT-Shawn",Artist="elguitarTom",Title="Shawn's Got The Shotgun")
     MyMusic(39)=(PL=2,Wave=11,Song="EGT-SignOfEvil",Artist="elguitarTom",Title="Sign Of Evil")
     strPlayingSong="Now playing: "
     FlareCloudClass=Class'ScrnBalanceSrv.ScrnFlareCloud'
     LobbyMenuClassString="ScrnBalanceSrv.ScrnLobbyMenu"
     PawnClass=Class'ScrnBalanceSrv.ScrnHumanPawn'
     bNotifyLocalPlayerTeamReceived=True
}
