 class NicePack extends Mutator
    dependson(NiceStorageServer)
    config(NicePack);
// Should we scale health off all zeds to 6-player level?
var config bool     bScaleZedHealth;
// Should we replace all pickups with their Nice versions when available?
var config bool     bReplacePickups;
// Settings for initial trader
var config bool     bInitialTrader;         // Use initial trader system?
var config bool     bStillDuringInitTrader; // Force players to stand still during initial trader
var config int      initialTraderTime;      // How much time should be allowed for initial trade?
// Progressive dosh config
var config bool         bUseProgresiveCash;                                                                 // Use progressive dosh system?
var config array<int>   waveCash;
var config float        lateMultiplier;
// Experience-conversion controlling variables
var config bool     bConvertExp;                // Should we even convert old exp into a new one?
var config float    vetFieldMedicExpCost;       // Amount of exp per HP healed
var config float    vetFieldMedicDmgExpCost;    // Amount of exp per 1 medic damage
var config float    vetSharpHeadshotExpCost;    // Amount of exp per head-shot
var config float    vetSupportDamageExpCost;    // Amount of exp per 1 shotgun damage
var config float    vetCommandoDamageExpCost;   // Amount of exp per 1 assault rifle damage
var config float    vetDemoDamageExpCost;       // Amount of exp per 1 explosive damage
var config float    vetZerkDamageExpCost;       // Amount of exp per 1 melee damage
var config float    vetHeavyMGDamageExpCost;    // Amount of exp per 1 heavy machine gun damage
var config float    vetGunslingerKillExpCost;   // Amount of exp per 1 assault rifle damage
// Allow changing skills at any time
var config bool     bAlwaysAllowSkillChange;
// Variables for controlling how zeds spawn
var config float    minSpawnRate, maxSpawnRate; // Minimum and maximum allowed spawn rate
var config int      minimalSpawnDistance;       // Minimal distance between ZedVolume and players that should allow for zeds to spawn
// FF voting - related settings
var config bool     bOneFFIncOnly;              // Option that only allows one FF increase per game
var config bool     bNoLateFFIncrease;          // Disables FF increase through voting after 1st wave
// Configuration variables that store whether or not to replace the specimen with it's mean counterpart
var config bool     bReplaceCrawler, bReplaceStalker, bReplaceClot, bReplaceGorefast, bReplaceBloat, bReplaceSiren, bReplaceHusk, bReplaceScrake, bReplaceFleshpound;
var config int      bigZedMinHealth;            // If zed's base Health >= this value, zed counts as Big
var config int      mediumZedMinHealth;

var int     maxPlayersInGame;

// 'Adrenaline junkie' zed-time extensions
var int junkieDoneGoals;        // How many times we collected enough head-shots to trigger zed-time extension
var int junkieNextGoal;         // How many head-shots we need for next zed-time extension
var int junkieDoneHeadshots;    // How many head-shot in a row was done from the start of last zed-time
var array<String> replCaps;
var bool bFFWasIncreased;
var int nextSirenScreamID;
var int stuckCounter;
// Max dead bodies among all players
var int maxDeadBodies;
var int deadBodyCounter;
var ScrnBalance ScrnMut;
var ScrnGameType ScrnGT;
var NiceGameType NiceGT;
var NicePack Mut;
var NiceRules GameRules;
var NiceStorageServer serverStorage;
var bool interactionAdded;
var bool bIsPreGame;
var bool bIsTraderTime;
var bool bWasZedTime;
var array<KFHumanPawn>  recordedHumanPawns;
struct PlayerRecord{
    var string  steamID;
    var bool    bHasSpawned;
    var int     lastCashWave;   //Last wave in which player either participated, or for which he received cash
    var int     kills, assists, deaths;
};
var array<PlayerRecord> PlayerDatabase;
// Zed hardcore level record
struct ZedRecord{
    var string ZedName;
    var class<NiceMonster> ZedType;
    var class<NiceMonster> MeanZedType;
    var bool bAlreadySpawned;
    var bool bMeanAlreadySpawned;
    var float HL;
    var float MeanHLBonus;
    var bool bNeedsReplacement;
};
var int lastStandardZed;
var array<ZedRecord> ZedDatabase;
struct NicePickupReplacement{
    var class<Pickup> vanillaClass;
    var class<Pickup> scrnClass;
    var class<Pickup> newClass;
};
var array<NicePickupReplacement> pickupReplaceArray;
struct CounterDisplay{
    var string              cName;
    var Texture             icon;
    var int                 value;
    var bool                bShowZeroValue;
    var class<NiceSkill>    ownerSkill;
};
var array<CounterDisplay> niceCounterSet;

// Replication of config between player and server
var int SrvFlags;
var array<NicePlayerController> playersList;
var NicePathBuilder globalPathBuilder;

replication{
    reliable if(Role == ROLE_Authority)
       SrvFlags, bIsPreGame, junkieDoneHeadshots, junkieNextGoal;
}

static function NicePathBuilder GetPathBuilder(){
    if(default.globalPathBuilder == none)
        default.globalPathBuilder = new() class'NicePathBuilder';
    return default.globalPathBuilder;
}

static final function NiceStorageBase GetStorage(LevelInfo level){
    local NicePlayerController localPlayer;
    if(default.serverStorage != none) return default.serverStorage;
    localPlayer = NicePlayerController(level.GetLocalPlayerController());
    if(localPlayer != none)
        return localPlayer.storageClient;
    return none;
}

static final function NicePack Myself(LevelInfo Level){
    local Mutator M;
    local NicePack NicePackMutator;
    if(default.Mut != none)
       return default.Mut;
    if (level == none) {
        return none;
    }
    // server-side
    if(Level != none && Level.Game != none){
       for(M = Level.Game.BaseMutator;M != none;M = M.NextMutator){
           NicePackMutator = NicePack(M);
           if(NicePackMutator != none){
               default.Mut = NicePackMutator;
               return NicePackMutator;
           }
       }
    }
    // client-side
    foreach Level.DynamicActors(class'NicePack', NicePackMutator){
       default.Mut = NicePackMutator;
       return NicePackMutator;
    }
    return none;
}
function PreBeginPlay()
{
    local ZombieVolume ZV;
    super.PreBeginPlay();
    foreach AllActors(Class'ZombieVolume', ZV)
       ZV.MinDistanceToPlayer = minimalSpawnDistance;
    AddToPackageMap("NicePackA.ukx");
    AddToPackageMap("NicePackSM.usx");
    AddToPackageMap("NicePackSnd.uax");
    AddToPackageMap("NicePackT.utx");
}

simulated function PostBeginPlay()
{
    local ScrnVotingHandlerMut VH;
    local MeanVoting VO;
    local NiceFFVoting FFVO;

    super.PostBeginPlay();
    Myself(level); // Record this copy

    class'ScrnLightVestPickup'.default.cost = 50;
    class'ScrnHorzineVestPickup'.default.weight = 2;
    class'ScrnHorzineVestPickup'.default.cost = 750;
    class'NicePack'.default.Mut = self;

    // Abilities
    class'NiceAbilityManager'.default.events.static.AddAdapter(class'NiceSharpshooterAbilitiesAdapter', level);
    class'NiceAbilityManager'.default.events.static.AddAdapter(class'NiceEnforcerAbilitiesAdapter', level);

    SetTimer(0.25, true);
    if (Role < ROLE_Authority)
       return;
    //  Create sync node
    serverStorage = new class'NiceStorageServer';
    default.serverStorage = serverStorage;
    serverStorage.events.static.AddAdapter(class'NiceRemoteDataAdapter', Level);
    // Find game type and ScrN mutator
    ScrnGT = ScrnGameType(Level.Game);
    NiceGT = NiceGameType(Level.Game);
    if (NiceGT == none)
    {
       Log("ERROR: Wrong GameType (requires NiceGameType)", class.Outer.Name);
       Destroy();
       return;
    }
    // FIXME poosh forces this variable in his mutator...
    NiceGT.LoginMenuClass = string(class'NiceInvasionLoginMenu');
    NiceGT.RegisterMutator(self);
    ScrnMut = NiceGT.ScrnBalanceMut;
    if(bReplacePickups)
       ScrnMut.bReplacePickups = false;
    // Replication of some variables
    SetReplicationData();
    // Game rules
    GameRules = Spawn(class'NiceRules', self);
    // -- Lower starting HL
    ScrnMut.GameRules.HardcoreLevel -= 7;
    ScrnMut.GameRules.HardcoreLevelFloat -= 7;

    // -- Fill-in zed info
    // - Clot
    ZedDatabase[0].bNeedsReplacement = bReplaceClot;
    // - Crawler
    ZedDatabase[1].bNeedsReplacement = bReplaceCrawler;
    // - Stalker
    ZedDatabase[2].bNeedsReplacement = bReplaceStalker;
    // - Gorefast
    ZedDatabase[3].bNeedsReplacement = bReplaceGorefast;
    // - Bloat
    ZedDatabase[4].bNeedsReplacement = bReplaceBloat;
    // - Siren
    ZedDatabase[5].bNeedsReplacement = bReplaceSiren;
    // - Husk
    ZedDatabase[6].bNeedsReplacement = bReplaceHusk;
    // - Scrake
    ZedDatabase[7].bNeedsReplacement = bReplaceScrake;
    // - Fleshpound
    ZedDatabase[8].bNeedsReplacement = bReplaceFleshpound;
    // FIXME!!!
    lastStandardZed = 8;

    // Add voting for mean zeds
    VH = class'ScrnVotingHandlerMut'.static.GetVotingHandler(Level.Game);
    if(VH == none){
       Level.Game.AddMutator(string(class'ScrnVotingHandlerMut'), false);
       VH = class'ScrnVotingHandlerMut'.static.GetVotingHandler(Level.Game);
    }
    if(VH != none){
       VO = MeanVoting(VH.AddVotingOptions(class'MeanVoting'));
       if(VO != none)
           VO.Mut = self;
       FFVO = NiceFFVoting(VH.AddVotingOptions(class'NiceFFVoting'));
       if(FFVO != none)
           FFVO.Mut = self;
    }
    else
       log("Unable to spawn voting handler mutator", class.outer.name);
}

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

    if (Role < ROLE_Authority)
       LoadReplicationData();
}

simulated function LoadReplicationData()
{
    bInitialTrader          = (SrvFlags & 0x00000001) > 0;
    bStillDuringInitTrader  = (SrvFlags & 0x00000002) > 0;
}

function SetReplicationData()
{
    SrvFlags = 0;
    if (bInitialTrader)
       SrvFlags = SrvFlags | 0x00000001;
    if (bStillDuringInitTrader)
       SrvFlags = SrvFlags | 0x00000002;
}

simulated function Timer(){
    local KFHumanPawn nextPawn;
    local int currentPlayersMax;
    local Controller P;
    local NicePlayerController nicePlayer;
    // Cull excessive pawns
    if(Role < Role_AUTHORITY){
       recordedHumanPawns.Length = 0;
       foreach DynamicActors(class'KFHumanPawn', nextPawn)
           if(nextPawn != none && nextPawn.health > 0)
               recordedHumanPawns[recordedHumanPawns.Length] = nextPawn;
       return;
    }
    // Broadcast skills & record latest player controller list
    BroadcastSkills();
    playersList.length = 0;
    for(P = Level.ControllerList; P != none; P = P.nextController){
       nicePlayer = NicePlayerController(P);
       if(nicePlayer != none){
           nicePlayer.wallHitsLeft = 10;
           //nicePlayer.FreeOldStuckBullets();
           playersList[playersList.Length] = nicePlayer;
           if(nicePlayer.Pawn != none && nicePlayer.Pawn.health > 0 && !nicePlayer.PlayerReplicationInfo.bIsSpectator
               && !nicePlayer.PlayerReplicationInfo.bOnlySpectator)
               currentPlayersMax ++;
       }
    }
    maxPlayersInGame = Max(maxPlayersInGame, currentPlayersMax);
}

// initial tick, does some job and shuts down
auto state loadInteractionnSpawnRate
{
    // state tick, overrides global
    simulated function Tick(float Delta)
    {
        local NiceInteraction niceInt;
        local NicePlayerController localPlayer;

        if (level.netMode == NM_DedicatedServer)
        {
            // set spawn rate!
            if (ScrnMut != none)
                ScrnMut.OriginalWaveSpawnPeriod = FMax(minSpawnRate, FMin(maxSpawnRate, ScrnMut.OriginalWaveSpawnPeriod));
            // do not execute below code on dedicated servers!
            GoToState('');
            DisableServerGlobalTick();
            return;
        }

        localPlayer = NicePlayerController(Level.GetLocalPlayerController());
        if (localPlayer == none)
            return;

        // spawn some magic
        if (localPlayer.storageClient != none && localPlayer.remoteRI != none)
        {
            localPlayer.storageClient.remoteRI = localPlayer.remoteRI;
            localPlayer.storageClient.events.static.CallLinkEstablished();
        }

        // Actually add the interaction
        niceInt = NiceInteraction(localPlayer.Player.InteractionMaster.AddInteraction(string(class'NiceInteraction'), localPlayer.Player));
        niceInt.RegisterMutator(Self);

        // break this state and go to global
        GoToState('');
    }
}

// disable tick for servers!
final private function DisableServerGlobalTick()
{
    Disable('Tick');
}

// global tick
simulated function Tick(float Delta)
{
    local int i;
    local NicePlayerController localPlayer;

    localPlayer = NicePlayerController(Level.GetLocalPlayerController());
    // Check if the local PlayerController is available yet
    if (localPlayer == none)
       return;

    if (localPlayer.bFlagDisplayCounters)
    {
        for (i = 0; i < niceCounterSet.Length; i++)
        {
            if (niceCounterSet[i].ownerSkill == none)
                niceCounterSet[i].value = UpdateCounterValue(niceCounterSet[i].cName);
            else if(class'NiceVeterancyTypes'.static.hasSkill(localPlayer, niceCounterSet[i].ownerSkill))
                niceCounterSet[i].value = niceCounterSet[i].ownerSkill.static.
                   UpdateCounterValue(niceCounterSet[i].cName, localPlayer);
            else
                niceCounterSet[i].value = 0;
        }
    }
    // Reset tick counter for traces
    localPlayer.tracesThisTick = 0;
    // Manage resetting of effects' limits
    if (Level.TimeSeconds >= localPlayer.nextEffectsResetTime)
    {
        localPlayer.nextEffectsResetTime = Level.TimeSeconds + 0.1;
        localPlayer.currentEffectTimeWindow ++;
        if (localPlayer.currentEffectTimeWindow >= 10)
            localPlayer.currentEffectTimeWindow = 0;
        localPlayer.effectsSpawned[localPlayer.currentEffectTimeWindow] = 0;
    }
}

simulated function bool CheckReplacement(Actor Other, out byte bSuperRelevant){
    local int i;
    local NiceMonster niceMonster;
    local NiceZombieBoss boss;
    local Controller cIt;
    local int currNumPlayers;
    local NicePlayerController playerContr;
    local NiceRepInfoRemoteData remoteRI;
    local NiceReplicationInfo niceRI;
    local PlayerReplicationInfo pri;
    // Replace loot on levels
    if(Other.class == class'KFRandomItemSpawn' || Other.class == class'ScrnRandomItemSpawn'){
       ReplaceWith(Other, string(class'NiceRandomItemSpawn'));
       return false;
    }
    else if(Other.class == class'KFAmmoPickup' || Other.class == class'ScrnAmmoPickup') {
       ReplaceWith(Other, string(class'NiceAmmoPickup'));
       return false;
    }
    else if(bReplacePickups && Pickup(Other) != none){
       i = FindPickupReplacementIndex(Pickup(Other));
       if (i != -1){
           ReplaceWith(Other, String(pickupReplaceArray[i].NewClass));
           return false;
       }
       return true;
    }
    // Add our replication info
    if(PlayerReplicationInfo(Other) != none && NicePlayerController(PlayerReplicationInfo(Other).Owner) != none){
       pri = PlayerReplicationInfo(Other);
       niceRI = spawn(class'NiceReplicationInfo', pri.Owner);
       niceRI.Mut = self;
       remoteRI = spawn(class'NiceRepInfoRemoteData', pri.Owner);
       playerContr = NicePlayerController(PlayerReplicationInfo(Other).Owner);
       playerContr.niceRI = niceRI;
       playerContr.remoteRI = remoteRI;
    }
    niceMonster = NiceMonster(Other);
    if(niceMonster != none){
        if (bUseProgresiveCash) {
            niceMonster.ScoringValue = 0;
        }
    }
    // Replace zeds with a healthier ones.
    // Code taken from a scary ghost's SpecimenHPConfig
    if(!bScaleZedHealth)
       return true;
    boss = NiceZombieBoss(Other);
    if(niceMonster != none){
       for(cIt= Level.ControllerList; cIt != none; cIt= cIt.NextController)
           if(cIt.bIsPlayer && cIt.Pawn != none && cIt.Pawn.Health > 0)
               currNumPlayers++;
       if(boss == none) {
           niceMonster.Health *= hpScale(niceMonster.PlayerCountHealthScale) / niceMonster.NumPlayersHealthModifer();
           niceMonster.HealthMax = niceMonster.Health;
           niceMonster.HeadHealth *= hpScale(niceMonster.PlayerNumHeadHealthScale) / niceMonster.NumPlayersHeadHealthModifer();
           niceMonster.HeadHealthMax = niceMonster.HeadHealth;
           if(Level.Game.NumPlayers == 1){
               niceMonster.MeleeDamage /= 0.75;
               niceMonster.ScreamDamage /= 0.75;
               niceMonster.SpinDamConst /= 0.75;
               niceMonster.SpinDamRand /= 0.75;
           }
       }
    }
    return true;
}
// returns -1, if not found
function int FindPickupReplacementIndex(Pickup item)
{
    local int i;
    for(i=0; i < pickupReplaceArray.length;i ++){
       if(pickupReplaceArray[i].vanillaClass == item.class || pickupReplaceArray[i].scrnClass == item.class)
           return i;
    }
    return -1;
}
// Try to extend zed-time, junkie-style
function JunkieZedTimeExtend(){
    if((ScrnGT != none && !ScrnGT.bZEDTimeActive) || ScrnGT.CurrentZEDTimeDuration <= 0)
       return;
    junkieDoneHeadshots ++;
    if(junkieNextGoal <= junkieDoneHeadshots){
       junkieDoneHeadshots = 0;
       junkieDoneGoals ++;
       junkieNextGoal ++;
       ScrnGT.DramaticEvent(1.0);
    }
}
simulated function AddCounter(string cName, Texture icon, optional bool bShowZeroValue,
    optional class<NiceSkill> owner){
    local CounterDisplay newCounter;
    RemoveCounter(cName);
    newCounter.cName = cName;
    newCounter.icon = icon;
    newCounter.bShowZeroValue = bShowZeroValue;
    newCounter.ownerSkill = owner;
    niceCounterSet[niceCounterSet.Length] = newCounter;
}
simulated function RemoveCounter(string cName){
    local int i;
    local array<CounterDisplay> newCounterSet;
    for(i = 0;i < niceCounterSet.Length;i ++)
       if(niceCounterSet[i].cName != cName)
           newCounterSet[newCounterSet.Length] = niceCounterSet[i];
    niceCounterSet = newCounterSet;
}
simulated function int GetVisibleCountersAmount(){
    local int i;
    local int amount;
    for(i = 0;i < niceCounterSet.Length;i ++)
       if(niceCounterSet[i].value != 0 || niceCounterSet[i].bShowZeroValue)
           amount ++;
    return amount;
}
simulated function int UpdateCounterValue(string cName){
    return 0;
}

// Returns cash per wave based on current difficulty
// Returns cash per wave based on current difficulty
function int GetWaveCash(int lastCashWave, int nextWave){
    local int i;
    local int accumulatedDosh;
    for (i = lastCashWave; i < nextWave; i += 1) {
        accumulatedDosh += waveCash[i];
    }
    if (lastCashWave + 1 < nextWave) {
        accumulatedDosh *= lateMultiplier;
    }
    return accumulatedDosh;
}
// Gives out appropriate (for the wave he entered) amount of dosh to the player
function GiveProgressiveDosh(NicePlayerController nicePlayer){
    local int           nextWave;
    local PlayerRecord  record;
    // Too early to give dosh
    if(!ScrnGT.IsInState('MatchInProgress'))
       return;
    // Real spectators shouldn't be affected
    if(nicePlayer == none) return;
    if(nicePlayer.PlayerReplicationInfo.bIsSpectator) return;
    if(nicePlayer.PlayerReplicationInfo.bOnlySpectator) return;
    record = FindPlayerRecord(nicePlayer.steamID64);
    nextWave = ScrnGT.WaveNum + 1;
    nicePlayer.PlayerReplicationInfo.Score += GetWaveCash(record.lastCashWave, nextWave);
    record.lastCashWave = nextWave;
    UpdatePlayerRecord(record);
}

simulated function Mutate(string MutateString, PlayerController kfPlayer)
{
    local int i, readLenght;
    local NicePlayerController nicePlayer;
    local NiceServerData remoteData;
    // Tokens from 'MutateString'
    local array<String> wordsArray;
    local String command, mod;
    local String white;
    local BitStreamWriter inputStream;
    local BitStreamReader outputStream;
    // Array with command modifiers.
    // Always contains at least 10 elements, that may be empty strings if there wasn't enough modifiers.
    // Done for safe access without the need to check for bounds.
    local array<String> modArray;

    super.Mutate(MutateString, kfPlayer);

    // Helpful sequence
    white = chr(27)$chr(200)$chr(200)$chr(200);
    // Transform our command into array for convenience
    Split(MutateString, " ", wordsArray);
    // Exit if command is empty
    if(wordsArray.Length == 0)
       return;
    // Fancier access
    command = wordsArray[0];
    if(wordsArray.Length > 1)
       mod = wordsArray[1];
    else
       mod = "";
    i = 0;
    while(i + 1 < wordsArray.Length || i < 10){
       if(i + 1 < wordsArray.Length)
           modArray[i] = wordsArray[i+1];
       else
           modArray[i] = "";
       i ++;
    }
    nicePlayer = NicePlayerController(kfPlayer);
    if(command ~= "ECHO")
       kfPlayer.ClientMessage(Mid(MutateString, 5));
    else if(command ~= "ZEDTIME" && bAlwaysAllowSkillChange)
       NiceGT.zedTimeHandler.ActivateZedTime();
    else if(command ~= "SAVECFG" && nicePlayer != none)
       nicePlayer.ClientSaveConfig();
    else if(command ~= "CONFIG" && nicePlayer != none){
       if(nicePlayer.bFlagAltSwitchesModes)
           nicePlayer.ClientMessage(white$"Alt fire button will switch between single and burst modes for assault rifles");
       else
           nicePlayer.ClientMessage(white$"Alt fire button will shoot either single or burst mode for assault rifles");
       if(nicePlayer.bFlagShowHLMessages)
           nicePlayer.ClientMessage(white$"Messages about HL change will be displayed for you");
       else
           nicePlayer.ClientMessage(white$"Messages about HL change will be hidden from you");
    }
    else if(command ~= "HLMESSAGES" && nicePlayer != none){
       if(mod ~= "ON")
           nicePlayer.ServerSetHLMessages(true);
       else if(mod ~= "OFF")
           nicePlayer.ServerSetHLMessages(false);
    }
    else if(command ~= "ALTSWITCH"){
       if(mod ~= "ON")
           nicePlayer.ServerSetAltSwitchesModes(true);
       else if(mod ~= "OFF")
           nicePlayer.ServerSetAltSwitchesModes(false);
    }
    else if(command ~= "SETKEY" && nicePlayer != none){
       if(Int(mod) > 0)
           nicePlayer.ClientSetKey(Int(mod));
    }
    else if(command ~= "NICEWEAPMANAGE" && nicePlayer != none){
       if(mod ~= "ON")
           nicePlayer.ClientSetNiceWeapManagement(true);
       else if(mod ~= "OFF")
           nicePlayer.ClientSetNiceWeapManagement(false);
    }
    else if(command ~= "DEBUG" && nicePlayer != none){
       if(mod ~= "ON")
           nicePlayer.ServerSetDebug(true);
       else if(mod ~= "OFF")
           nicePlayer.ServerSetDebug(false);
    }
    else if(command ~= "LOGLINE" && nicePlayer != none)
       nicePlayer.ClientLog("UserLine:"$mod);
    else if(command ~= "CREATE"){
        nicePlayer.ClientMessage("ATTEMPT"@string(serverStorage));
        serverStorage.CreateData(modArray[0], NSP_HIGH);
        remoteData = NiceServerData(serverStorage.GetData(modArray[0]));
        remoteData.isAdminOnly = true;
        nicePlayer.ClientMessage("ATTEMPT2"@string(remoteData));
    }
    else if(command ~= "CREATELOW"){
        serverStorage.CreateData(modArray[0], NSP_LOW);
    }
    else if(command ~= "SET"){
        remoteData = NiceServerData(serverStorage.GetData(modArray[0]));
        nicePlayer.ClientMessage("SETATTEMPT"@string(remoteData));
        remoteData.SetInt(modArray[1], Int(modArray[2]));
        nicePlayer.ClientMessage("SETATTEMPT 2"@modArray[1]@modArray[2]);
    }
    else if(command ~= "PRINT"){
        nicePlayer.ClientPrint();
    }
    else if(command ~= "TEST"){
        inputStream = new class'BitStreamWriter';
        outputStream = new class'BitStreamReader';
        inputStream.WriteInt(Len(mod), 5);
        inputStream.WriteClassName(mod);
        outputStream.Initialize(inputStream.GetData());
        readLenght = outputStream.ReadInt(5);
        nicePlayer.ClientMessage("Input lenght:" @ string(Len(mod)));
        nicePlayer.ClientMessage("Compressed lenght:" @ string(inputStream.GetSizeInBytes()) );
        nicePlayer.ClientMessage("Output:"@outputStream.ReadClassName(readLenght));
    }
}
/* Good test for writer
else if(command ~= "TEST"){
        inputStream = new class'BitStreamWriter';
        outputStream = new class'BitStreamReader';
        //stream.PushByte(167, int(mod));
        inputStream.WriteInt(3, 3);
        inputStream.WriteByte(49, 7);
        inputStream.WriteInt(1651779982, 25);
        inputStream.WriteInt(2, 2);
        bytes = inputStream.GetData();
        bits = inputStream.GetSize();
        nicePlayer.ClientMessage("SIZE:" @ string(bits));
        for (i = 0; i < bytes.length; i += 1) {
            nicePlayer.ClientMessage("Content:" @ string(bytes[i]));
        }
        outputStream.Initialize(bytes);
        //nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(3)));
        //nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(7)));
        nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(8)));
        nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(4)));
        nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(4)));
        outputStream.Initialize(bytes);
        nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(3)));
        nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(7)));
        nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(5)));
        outputStream.Initialize(bytes);
        nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadInt(3)));
        nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadByte(7)));
        nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadInt(25)));
        nicePlayer.ClientMessage("OUT:" @ string(outputStream.ReadInt(2)));
    }
 */
// Event functions
// Called at the start of the match
function MatchBegan(){
}
// Called when new wave begins
function WaveStart(){
    local Controller P;
    local NiceHumanPawn nicePawn;
    local NicePlayerController nicePlayer;
    bIsPreGame = false;
    bIsTraderTime = false;
    for(P = Level.ControllerList; P != none; P = P.nextController){
       nicePlayer = NicePlayerController(P);
       if(nicePlayer != none){
           // Give out armor
           nicePawn = NiceHumanPawn(nicePlayer.Pawn);
           if(nicePawn != none && nicePawn.Health > 0){
               nicePawn.bGotFreeJacket = false;
               nicePawn.getFreeJacket();
               nicePawn.bReactiveArmorUsed = false;
           }
       }
    }
}
// Called when trader time begins
simulated function TraderStart(){
    local Controller P;
    local NiceHumanPawn nicePawn;
    local NicePlayerController nicePlayer;
    bIsTraderTime = true;
    for(P = Level.ControllerList; P != none; P = P.nextController){
       nicePlayer = NicePlayerController(P);
       if(nicePlayer != none){
           nicePlayer.TryActivatePendingSkills();
           nicePlayer.ClientSaveConfig();
           nicePawn = NiceHumanPawn(nicePlayer.Pawn);
           if (bUseProgresiveCash) {
               GiveProgressiveDosh(nicePlayer);
           }
       }
    }
}
// Called when zed-time begins
simulated function ZedTimeActivated(){
}
// Called when zed-time deactivated
simulated function ZedTimeDeactivated(){
    local Controller P;
    local NicePlayerController Player;
    junkieNextGoal=1;
    junkieDoneGoals=0;
    junkieDoneHeadshots=0;
    for(P = Level.ControllerList; P != none; P = P.nextController){
       Player = NicePlayerController(P);
       if(Player != none)
           Player.bJunkieExtFailed = false;
    }
}
// Returns player record, corresponding to the given steam id
function PlayerRecord FindPlayerRecord(string steamID){
    local int i;
    local PlayerRecord newRecord;
    for(i = 0;i < PlayerDatabase.Length;i ++)
       if(PlayerDatabase[i].steamID == steamID)
           return PlayerDatabase[i];
    newRecord.steamID = steamID;
    newRecord.bHasSpawned = false;
    newRecord.lastCashWave = 0;
    newRecord.kills = 0;
    newRecord.assists = 0;
    newRecord.deaths = 0;
    PlayerDatabase[PlayerDatabase.Length] = newRecord;
    return newRecord;
}
// Updates existing PlayerRecord (with a same steam id) and adds a new one, if necessary (record with a same steam is not found)
function UpdatePlayerRecord(PlayerRecord record){
    local int i;
    for(i = 0;i < PlayerDatabase.Length;i ++)
       if(PlayerDatabase[i].steamID == record.steamID){
           PlayerDatabase[i] = record;
           return;
       }
    PlayerDatabase[PlayerDatabase.Length] = record;
}
// Checks if it should be possible to change skills right now
function bool CanChangeSkill(NicePlayerController player){
    local PlayerRecord record;
    record = FindPlayerRecord(player.SteamID64);
    return (bIsTraderTime || (bIsPreGame && bInitialTrader) || bAlwaysAllowSkillChange || !record.bHasSpawned);
}
// Outputs info about given skill in console
function DisplaySkill(class<NiceSkill> skill, int level, bool selected, PlayerController player){
    local String skillColor;
    local String white;
    if(selected)
       skillColor = chr(27)$chr(1)$chr(200)$chr(1);
    else
       skillColor = chr(27)$chr(200)$chr(1)$chr(1);
    white = chr(27)$chr(200)$chr(200)$chr(200);
    player.ClientMessage(white$"Level"@String(level)$skillColor@"skill"$white$":"@skill.default.SkillName);
    // Just in case description is too long
    player.ClientMessage("    ");
}
function UpdateHealthLevels(){
    local Controller P, S;
    local NiceHumanPawn updatePawn;
    for(P = Level.ControllerList;P != none;P = P.nextController){
       updatePawn = NiceHumanPawn(P.Pawn);
       if(updatePawn == none)
           continue;
       updatePawn.HealthMax = (updatePawn.default.HealthMax + updatePawn.HealthBonus) * updatePawn.ScrnPerk.static.HealthMaxMult(KFPlayerReplicationInfo(P.PlayerReplicationInfo), updatePawn);
       for(S = Level.ControllerList;S != none;S = S.nextController)
           if(NicePlayerController(S) != none)
               NicePlayerController(S).ClientUpdatePawnMaxHealth(updatePawn, updatePawn.HealthMax);
    }
}
function BroadcastSkills(){
    local int i, j;
    local bool bSameSkillFound;
    local Controller P;
    local NicePlayerController nicePlayer;
    local array< class<NiceSkill> > playerSkills;
    // Skills to broadcast
    local array<int>                teamNumbers;
    local array< class<NiceSkill> > skillsToSend;
    for(P = Level.ControllerList;P != none;P = P.nextController){
       nicePlayer = NicePlayerController(P);
       if(nicePlayer != none){
           playerSkills = nicePlayer.GetActiveBroadcastSkills();
           // Process player's skills
           for(i = 0;i < playerSkills.Length;i ++){
               bSameSkillFound = false;
               // Try to find if someone already shares the same skill in the same team
               for(j = 0;j < skillsToSend.Length && !bSameSkillFound;j ++)
                   if(playerSkills[i] == skillsToSend[j] && teamNumbers[j] == nicePlayer.PlayerReplicationInfo.Team.TeamIndex)
                       bSameSkillFound = true;
               // If not - add it
               if(!bSameSkillFound){
                   teamNumbers[teamNumbers.Length] = nicePlayer.PlayerReplicationInfo.Team.TeamIndex;
                   skillsToSend[skillsToSend.Length] = playerSkills[i];
               }
           }
       }
    }
    for(P = Level.ControllerList;P != none;P = P.nextController){
       nicePlayer = NicePlayerController(P);
       if(nicePlayer != none){
           for(i = 0;i < skillsToSend.Length;i ++)
               if(teamNumbers[i] == nicePlayer.PlayerReplicationInfo.Team.TeamIndex)
                   nicePlayer.ClientReceiveSkill(skillsToSend[i]);
           nicePlayer.ClientBroadcastEnded();
       }
    }
}

// Function for broadcasting messages to players
function BroadcastToAll(string message){
    local Controller P;
    local PlayerController Player;
    for(P = Level.ControllerList; P != none; P = P.nextController){
       Player = PlayerController(P);
       if(Player != none)
           Player.ClientMessage(message);
    }
}
// Function for finding number, corresponding to zed's name
function int ZedNumber(String ZedName){
    local int i;
    for(i = 0;i < ZedDatabase.Length;i ++)
       if(ZedName ~= ZedDatabase[i].ZedName)
           return i;
    return -1;
}
// Function for correct hp scaling
function float hpScale(float hpScale) {
    return 1.0 + 5.0 * hpScale;
}
static function FillPlayInfo(PlayInfo PlayInfo){
    Super.FillPlayInfo(PlayInfo);
    PlayInfo.AddSetting("NicePack", "bScaleZedHealth", "Scale zeds' health?", 0, 0, "check");
    PlayInfo.AddSetting("NicePack", "bReplacePickups", "Replace pickups?", 0, 0, "check");
    PlayInfo.AddSetting("NicePack", "bInitialTrader", "Use init trader?", 0, 0, "check");
    PlayInfo.AddSetting("NicePack", "bStillDuringInitTrader", "Be still during init trader?", 0, 0, "check");
    PlayInfo.AddSetting("NicePack", "initialTraderTime", "Time for init trader?", 0, 0, "text", "3;1:999");
    PlayInfo.AddSetting("NicePack", "bUseProgresiveCash", "Use progressive dosh?", 0, 0, "check");
    PlayInfo.AddSetting("NicePack", "bConvertExp", "Convert old exp?", 0, 0, "check");
    PlayInfo.AddSetting("NicePack", "bAlwaysAllowSkillChange", "Skill change at anytime?", 0, 0, "check");
    PlayInfo.AddSetting("NicePack", "minimalSpawnDistance", "Min spawn distance", 0, 0, "text", "5;0:99999");
    PlayInfo.AddSetting("NicePack", "minSpawnRate", "Min spawn rate", 0, 0, "text", "6;0.0:10.0");
    PlayInfo.AddSetting("NicePack", "maxSpawnRate", "Max spawn rate", 0, 0, "text", "6;0.0:10.0");
    PlayInfo.AddSetting("NicePack", "bOneFFIncOnly", "Only 1 FF increase?", 0, 0, "check");
    PlayInfo.AddSetting("NicePack", "bNoLateFFIncrease", "FF increase wave 1 only?", 0, 0, "check");
}
static function string GetDescriptionText(string SettingName){
    switch (SettingName){
       case "bScaleZedHealth":
           return "Should we scale health off all zeds to 6-player level?";
       case "bReplacePickups":
           return "Should we replace all pickups with their Nice versions when available?";
       case "bInitialTrader":
           return "Use initial trader system?";
       case "bStillDuringInitTrader":
           return "Force players to stand still during initial trader?";
       case "initialTraderTime":
           return "How much time should be allowed for initial trade?";
       case "bUseProgresiveCash":
           return "Use progressive dosh system?";
       case "bConvertExp":
           return "Should we convert old exp into a new one?";
       case "bAlwaysAllowSkillChange":
           return "Allows changing skills at any time.";
       case "minimalSpawnDistance":
           return "Minimal distance between ZedVolume and players that should allow for zeds to spawn.";
       case "minSpawnRate":
           return "Minimal allowed spawn rate.";
       case "maxSpawnRate":
           return "Maximal allowed spawn rate.";
       case "bOneFFIncOnly":
           return "Option that only allows one FF increase per game.";
       case "bNoLateFFIncrease":
           return "Disables FF increase through voting after 1st wave.";
    }
    return Super.GetDescriptionText(SettingName);
}

defaultproperties
{
    GroupName="KFNicePack"
    FriendlyName="Package for nice/mean servers"
    Description="Does stuff."
    bAlwaysRelevant=True
    RemoteRole=ROLE_SimulatedProxy
    bAddToServerPackages=True

    // used in scrn voting and HL calculation
    // TODO remove HL!
    ZedDatabase(00)=(ZedName="Clot",ZedType=class'NiceZombieClot',MeanZedType=class'MeanZombieClot',HL=0.0,MeanHLBonus=0.5)
    ZedDatabase(01)=(ZedName="Crawler",ZedType=class'NiceZombieCrawler',MeanZedType=class'MeanZombieCrawler',HL=0.5,MeanHLBonus=1.5)
    ZedDatabase(02)=(ZedName="Stalker",ZedType=class'NiceZombieStalker',MeanZedType=class'MeanZombieStalker',HL=0.5,MeanHLBonus=0.5)
    ZedDatabase(03)=(ZedName="Gorefast",ZedType=class'NiceZombieGorefast',MeanZedType=class'MeanZombieGorefast',HL=0.0,MeanHLBonus=0.5)
    ZedDatabase(04)=(ZedName="Bloat",ZedType=class'NiceZombieBloat',MeanZedType=class'MeanZombieBloat',HL=0.0,MeanHLBonus=0.5)
    ZedDatabase(05)=(ZedName="Siren",ZedType=class'NiceZombieSiren',MeanZedType=class'MeanZombieSiren',HL=1.0,MeanHLBonus=1.0)
    ZedDatabase(06)=(ZedName="Husk",ZedType=class'NiceZombieHusk',MeanZedType=class'MeanZombieHusk',HL=1.0,MeanHLBonus=1.5)
    ZedDatabase(07)=(ZedName="Scrake",ZedType=class'NiceZombieScrake',MeanZedType=class'MeanZombieScrake',HL=1.5,MeanHLBonus=1.5)
    ZedDatabase(08)=(ZedName="Fleshpound",ZedType=class'NiceZombieFleshPound',MeanZedType=class'MeanZombieFleshPound',HL=2.5,MeanHLBonus=1.5)
    ZedDatabase(09)=(ZedName="Shiver",ZedType=class'NiceZombieShiver',HL=1.0)
    ZedDatabase(10)=(ZedName="Jason",ZedType=class'NiceZombieJason',HL=1.5)
    ZedDatabase(11)=(ZedName="Tesla Husk",ZedType=class'NiceZombieTeslaHusk',HL=1.5)
    ZedDatabase(12)=(ZedName="Brute",ZedType=class'NiceZombieBrute',HL=2.0)
    ZedDatabase(13)=(ZedName="Ghost",ZedType=class'NiceZombieGhost',HL=0.5)
    ZedDatabase(14)=(ZedName="Sick",ZedType=class'NiceZombieSick',HL=1.0)

    bScaleZedHealth=True
    bReplacePickups=True
    bInitialTrader=True
    initialTraderTime=10
    bUseProgresiveCash=True
    waveCash(0)=1000
    waveCash(1)=900
    waveCash(2)=800
    waveCash(3)=650
    waveCash(4)=600
    waveCash(5)=450
    waveCash(6)=350
    waveCash(7)=250
    lateMultiplier=0.5
    bConvertExp=True
    vetFieldMedicExpCost=2.000000
    vetFieldMedicDmgExpCost=0.025000
    vetSharpHeadshotExpCost=10.000000
    vetSupportDamageExpCost=0.050000
    vetCommandoDamageExpCost=0.050000
    vetDemoDamageExpCost=0.050000
    vetZerkDamageExpCost=0.050000
    vetHeavyMGDamageExpCost=0.050000
    vetGunslingerKillExpCost=20.000000
    bigZedMinHealth=1000
    mediumZedMinHealth=500
    minSpawnRate=0.700000
    maxSpawnRate=1.500000
    minimalSpawnDistance=600
    bNoLateFFIncrease=True
    junkieNextGoal=1
    bIsPreGame=True

    // replace pickups that map spawn's itself
    pickupReplaceArray(00)=(vanillaClass=Class'KFMod.MAC10Pickup',scrnClass=Class'ScrnMAC10Pickup',NewClass=class'NiceMAC10Pickup')
    pickupReplaceArray(01)=(vanillaClass=Class'KFMod.WinchesterPickup',scrnClass=Class'ScrnWinchesterPickup',NewClass=class'NiceWinchesterPickup')
    pickupReplaceArray(02)=(vanillaClass=Class'KFMod.CrossbowPickup',scrnClass=Class'ScrnCrossbowPickup',NewClass=class'NiceCrossbowPickup')
    pickupReplaceArray(03)=(vanillaClass=Class'KFMod.SPSniperPickup',scrnClass=Class'ScrnSPSniperPickup',NewClass=class'NiceMaulerPickup')
    pickupReplaceArray(04)=(vanillaClass=Class'KFMod.M14EBRPickup',scrnClass=Class'ScrnM14EBRPickup',NewClass=class'NiceM14EBRPickup')
    pickupReplaceArray(05)=(vanillaClass=Class'KFMod.M99Pickup',scrnClass=Class'ScrnM99Pickup',NewClass=class'NiceM99Pickup')
    pickupReplaceArray(06)=(vanillaClass=Class'KFMod.ShotgunPickup',scrnClass=Class'ScrnShotgunPickup',NewClass=class'NiceShotgunPickup')
    pickupReplaceArray(07)=(vanillaClass=Class'KFMod.BoomStickPickup',scrnClass=Class'ScrnBoomStickPickup',NewClass=class'NiceBoomStickPickup')
    pickupReplaceArray(08)=(vanillaClass=Class'KFMod.NailGunPickup',scrnClass=Class'ScrnNailGunPickup',NewClass=class'NiceNailGunPickup')
    pickupReplaceArray(09)=(vanillaClass=Class'KFMod.KSGPickup',scrnClass=Class'ScrnKSGPickup',NewClass=class'NiceKSGPickup')
    pickupReplaceArray(10)=(vanillaClass=Class'KFMod.BenelliPickup',scrnClass=Class'ScrnBenelliPickup',NewClass=class'NiceBenelliPickup')
    pickupReplaceArray(11)=(vanillaClass=Class'KFMod.AA12Pickup',scrnClass=Class'ScrnAA12Pickup',NewClass=class'NiceAA12Pickup')
    pickupReplaceArray(12)=(vanillaClass=Class'KFMod.MachetePickup',scrnClass=Class'ScrnMachetePickup',NewClass=class'NiceMachetePickup')
}