AutoRespawnerMut

Тема в разделе "Общего назначения", создана пользователем Essence, 11 мар 2020.

  1. Essence

    Essence Moderator Команда форума

    Данный мутатор является результатом объединения таких мутаторов как AutoSpawner и RespawnMut

    Настройки:
    SpawnType - Способ возрождения:
    0 - Возрождение происходит в точке спавна игроков;
    1 - Возрождение рядом со случайным игроком;
    2 - Возрождение рядом с игроком с наибольшим кол-вом хп и брони;
    3 - Возрождение рядом с игроком с наибольшим уровнем.
    bAllowSpawnOnBossWave - Если True, то респавн во время волны с боссом разрешен.
    bUseGodModeWhileRespawn - Если True, то при авто-респавне игрок будет неуязвим UnbreakableTime секунд.
    bUseRespawner - Если True, то игрок сможет возродиться во время волны по прошествии RespawnTimeout секунд.
    UnbreakableTime - Сколько времени игрок будет неуязвим.
    RespawnTimeout - Время, через которое возродиться игрок.
    ThresholdList - Список пороговых соотношений для каждой волны, состоящий из двух значений:
    WaveNumber - Номер волны.
    RespawnLiveToDeathThreshold - Пороговое соотношение между живыми и мертвыми игроками при котором возможен респавн для этой волны.
    LiveToDeathThresholdMsgTime - Задержка между сообщениями о невозможности респавна.

    Код с комментариями:

    Код:
    // Written by Flame & Dave_Scream
    // Modified by Essence
    // Собственно, сам мутатор
    class AutoRespawnerMut extends Mutator Config(AutoRespawnerMut);
    
    // Структуры и массивы
    struct WaitingStruct
    {
        var string Hash;
        var PlayerController PC;
    };
    var array<WaitingStruct> WaitingList; // Лист ожидающих спавна
    
    struct ThresholdStruct
    {
        var config float RespawnLiveToDeathThreshold; // Пороговое соотношение между живыми и мертвыми игроками при котором возможен респавн
        var config int WaveNumber; // Номер волны для которой действует соответствующий порог
    };
    var config array<ThresholdStruct> ThresholdList; // Лист пороговых соотношений для каждой волны
    
    var array<string> SpawnedList; // Лист уже заспавненных
    var array<PlayerController> PendingPlayers; // Лист игроков
    
    // Флаги
    var config bool bUseGodModeWhileRespawn; // Будет ли игрок неуязвим при авто-респавне
    var config bool bAllowSpawnOnBossWave; // Разрешен ли респавн во время волны с боссом
    var config bool bUseRespawner; // Включен ли авто-респавн
    
    // Тип спавна: 0 - дефолтный, 1 - рандомный, 2 - в зависимости от кол-ва хп и брони, 3 - в зависимости от уровня
    var config byte SpawnType;
    
    // Время
    var config float LiveToDeathThresholdMsgTime; // Задержка между сообщениями о невозможности респавна
    var config float UnbreakableTime; // Сколько будет длиться неуязвимость
    var config int RespawnTimeout; // Время через которое игрок зареспавнится
    
    // Сообщения
    var config localized string MSG_BroadcastPlayerRespawned;
    var config localized string MSG_RespawnOnBossWave;
    var config localized string MSG_RespawnThreshold;
    var config localized string MSG_WaitToRespawn;
    var config localized string MSG_ReSpawned3;
    var config localized string MSG_ReSpawned2;
    var config localized string MSG_ReSpawned;
    
    var KFGameType KFGT; // ГТ
    
    function PostBeginPlay()
    {
        local AutoRespawnerRules ARL;
        if(bUseRespawner) // Если будем использовать авто-респавн на волне
        {
            if(ARL==None) ARL=Spawn(Class'AutoRespawnerRules'); // Спавним правила
            ARL.Mut=Self; // Ссылка на мут
        }
        KFGT=KFGameType(Level.Game);
    }
    
    // Отлавливаем игроков
    function bool CheckReplacement(Actor Other, out byte bSuperRelevant)
    {
        if(PlayerController(Other)!=None)
        {
            PendingPlayers[PendingPlayers.Length]=PlayerController(Other);
            SetTimer(0.1, False);
        }
        Return True;
    }
    
    function Timer()
    {
        local int i;
        local string Hash;
        local PlayerController PC;
        for(i=PendingPlayers.Length-1; i>=0; i--)
        {
            PC=PendingPlayers[i];
            if(PC!=None && PC.PlayerReplicationInfo.PlayerID>0)
            {
                // Если спавн игроков во время волны с боссом запрещен, то оповещаем об этом игрока
                if(!bAllowSpawnOnBossWave && KFGT.bWaveBossInProgress)
                {
                    // Upd. Успеет ли увидеть этот месседж игрок?
                    // PC.ClientMessage(MSG_RespawnOnBossWave);
                    PendingPlayers.Remove(i,1); // Удаляем из PendingPlayers
                    Continue; // Дальше не идем
                }
                Hash=PC.GetPlayerIDHash();
                // Если игрок не был заспавнен [и текущая волна не последняя], то добавляем игрока в лист ожидающих спавна
                if(!InSpawnedList(Hash)) AddInWaitingList(PC,Hash);
                // Если будем использовать авто-респавн на волне, то прикрепляем актера
                if(bUseRespawner) GiveRespawnerToPlayer(PC);
            }
        }
        PendingPlayers.Length=0;
    }
    
    // Проверка на наличие игрока в листе уже заспавненных
    function bool InSpawnedList(string Hash)
    {
        local int i;
        for(i=0; i<SpawnedList.Length; i++) if(SpawnedList[i]~=Hash) Return True;
        Return False;
    }
    
    // Заносим игрока в лист ожидающих спавна
    function AddInWaitingList(PlayerController PC, string Hash)
    {
        local WaitingStruct WS;
        if(!InWaitingList(Hash)) // Для предотвращения повторного добавления игрока
        {
            WS.Hash=Hash;
            WS.PC=PC;
            WaitingList[WaitingList.Length]=WS;
        }
    }
    
    // Проверка на наличие игрока в листе ожидающих спавна
    function bool InWaitingList(string Hash)
    {
        local int i;
        for(i=0; i<WaitingList.Length; i++) if(WaitingList[i].Hash~=Hash) Return True;
        Return False;
    }
    
    // Отслеживаем появление тела игрока
    function ModifyPlayer(Pawn Other)
    {
         // Если в настройках включена неуязвимость при респавне, то выдаем айтем
        if(bUseGodModeWhileRespawn) GiveItemToPlayer(Other);
        // Для совместимости с другими мутаторами
        if(NextMutator!=None) NextMutator.ModifyPlayer(Other);
    }
    
    // Стандартный код спавна игрока
    function SpawnWaitingPlayer(PlayerController PC)
    {
        local Pawn BestPlayer;
        KFGT.Disable('Timer');
        PC.PlayerReplicationInfo.bOutOfLives=False;
        PC.PlayerReplicationInfo.NumLives=0;
        PC.PlayerReplicationInfo.Score=Max(KFGT.MinRespawnCash, int(PC.PlayerReplicationInfo.Score));
        PC.GoToState('PlayerWaiting');
        PC.SetViewTarget(PC);
        PC.ClientSetBehindView(False);
        PC.bBehindView=False;
        PC.ClientSetViewTarget(PC.Pawn);
        if(KFGT.bWaveInProgress)
        {
            KFGT.bWaveInProgress=False;
            PC.ServerReStartPlayer();
            KFGT.bWaveInProgress=True;
        }
        else PC.ServerReStartPlayer();
        // Используем другой тип спавна, иначе игрок заспавнится в точке спавна игроков
        if(SpawnType>0)
        {
            BestPlayer=Class'AutoRespawnerUtilities'.Static.GetBestSaviour(Level, SpawnType, PC);
            PC.Pawn.SetLocation(BestPlayer.Location);
        }
        KFGT.Enable('Timer');
        // Информируем о том, что игрок заспавнился
        if(BestPlayer!=None)
        {
            if(bUseGodModeWhileRespawn)
                PC.ClientMessage(MSG_ReSpawned@MSG_ReSpawned2@BestPlayer.PlayerReplicationInfo.PlayerName@Repl(MSG_ReSpawned3,"%utime%",UnbreakableTime));
            else
                PC.ClientMessage(MSG_ReSpawned@MSG_ReSpawned2@BestPlayer.PlayerReplicationInfo.PlayerName);
            KFGT.Broadcast(Self,Repl(MSG_BroadcastPlayerRespawned,"%player%",PC.PlayerReplicationInfo.PlayerName)@MSG_ReSpawned2@BestPlayer.PlayerReplicationInfo.PlayerName);
        }
        else
        {
            if(bUseGodModeWhileRespawn)
                PC.ClientMessage(MSG_ReSpawned@Repl(MSG_ReSpawned3,"%utime%",UnbreakableTime));
            else
                PC.ClientMessage(MSG_ReSpawned);
            KFGT.Broadcast(Self,Repl(MSG_BroadcastPlayerRespawned,"%player%",PC.PlayerReplicationInfo.PlayerName));
        }
    }
    
    // Пытаемся заспавнить игроков
    function SpawnWaitingPlayers()
    {
        local int i;
        local PlayerController PC;
        for(i=WaitingList.Length-1; i>=0; i--)
        {
            PC=WaitingList[i].PC;
            // Если контроллер None, то игрок вышел с сервера, спавнить некого
            if(PC==None)
            {
                WaitingList.Remove(i,1); // Удаляем из WaitingList
                Continue; // Дальше не идем
            }
            // Игрок уже заспавнился (посредством ManageWaitingPlayer или через ГТ), заносим в SpawnedList и удаляем его из WaitingList
            if(PC.Pawn!=None)
            {
                SpawnedList[SpawnedList.Length]=WaitingList[i].Hash; // Заносим в SpawnedList
                WaitingList.Remove(i,1); // // Удаляем из WaitingList
                Continue; // Дальше не идем
            }
            // Если игрок зритель, то не трогаем его
            // Нужна ли здесь проверка на bOutOfLives?
            if(PC!=None && PC.PlayerReplicationInfo.bOutOfLives && !PC.PlayerReplicationInfo.bOnlySpectator)
                SpawnWaitingPlayer(PC);    // Спавним игрока
        }
    }
    
    // Если авто-респавн на волне включен - выдаем авто-респавнер
    function GiveRespawnerToPlayer(PlayerController PC)
    {
        local AutoRespawner AR;
        AR=Spawn(Class'AutoRespawner',PC);
        if(AR!=None)
        {
            AR.Mut=Self; // Ссылка на мут
            PC.Attached[PC.Attached.Length]=AR; // Прикрепляем к игроку
        }
    }
    
    // Если неуязвимость при спавне включена - выдаем айтем
    function GiveItemToPlayer(Pawn P)
    {
        local AutoRespawnerItem ARI;
        ARI=Spawn(Class'AutoRespawnerItem',P);
        if(ARI!=None)
        {
            ARI.Mut=Self; // Ссылка на мут
            P.Attached[P.Attached.Length]=ARI; // Прикрепляем к игроку
        }
    }
    
    // Получаем пороговое соотношение для текущей волны в случае использования авто-респавнера
    function float GetRespawnLiveToDeathThreshold(int CurWave)
    {
        local int i;
        for(i=0; i<ThresholdList.Length; i++) if(ThresholdList[i].WaveNumber==CurWave) Return ThresholdList[i].RespawnLiveToDeathThreshold;
        Return 0.5; // Лист пуст или для данной волны порог не задан, возвращаем 50/50
    }
    
    // Отслеживаем выход игрока с сервера
    function NotifyLogout(Controller Exiting)
    {
        local PlayerController PC;
        local int i;
        PC=PlayerController(Exiting);
        if(PC!=None)
        {
            for(i=0; i<PC.Attached.Length; i++)
            {
                if(PC.Attached[i].IsA('AutoRespawner'))
                {
                    PC.Attached[i].Destroy(); // Уничтожаем AutoRespawner закрепленный за игроком
                    Break;
                }
            }
        }
        if(NextMutator!=None) NextMutator.NotifyLogout(Exiting);
    }
    
    // Таймер для отслеживания игроков, нуждающихся в спавне
    Auto State MatchTimer
    {
    Begin:
        While(True)
        {
            Sleep(1.0);
            SpawnWaitingPlayers();
        }
    }
    
    defaultproperties
    {
        GroupName="KF-AutoRespawnerMut"
        FriendlyName="AutoRespawnerMut"
        Description="Allows newly joined players to spawn during the existing wave."
    }

    Код:
    // Written by Essence
    // Правила для отслеживания смерти игрока
    class AutoRespawnerRules extends GameRules;
    
    var AutoRespawnerMut Mut; // Ссылка на мут
    
    // Стандартные функции
    function PostBeginPlay()
    {
        if(Level.Game.GameRulesModifiers==None) Level.Game.GameRulesModifiers=Self;
        else Level.Game.GameRulesModifiers.AddGameRules(Self);
    }
    
    function AddGameRules(GameRules GR)
    {
        if(GR!=Self) Super.AddGameRules(GR);
    }
    //
    
    // Игрок умер, передаем эту информацию в AutoRespawner игрока
    function bool PreventDeath(Pawn Killed, Controller Killer, class<DamageType> DamageType, Vector HitLocation)
    {
        local int i;
        if(Killed!=None && Killed.IsA('KFHumanPawn') && Killed.Controller!=None && Killed.Controller.IsA('PlayerController'))
        {
            for(i=0; i<Killed.Controller.Attached.Length; i++)
            {
                if(Killed.Controller.Attached[i].IsA('AutoRespawner'))
                {
                    Killed.Controller.Attached[i].PawnBaseDied();
                    Break;
                }
            }
        }
        if(NextGameRules!=None) Return NextGameRules.PreventDeath(Killed, Killer, DamageType, HitLocation);
        Return False;
    }

    Код:
    // Written by Dave_Scream
    // Modified by Essence
    // Утилита для авто-респавна игрока
    class AutoRespawner extends Actor;
    
    var int RespawnCounter; // Счетчик респавна
    var float RespawnMsgDelay; // Задержка между сообщениями
    var AutoRespawnerMut Mut; // Ссылка на мут
    var PlayerController OwnerPC; // PlayerController владельца
    var float LiveToDeathThresholdMsgTimeNext; // Время следующего сообщения о невозможности респавна
    var KFGameType KFGT; // ГТ
    
    function PostBeginPlay()
    {
        KFGT=KFGameType(Level.Game);
        OwnerPC=PlayerController(Owner); // Запоминаем владельца
    }
    
    // Функция PawnBaseDied вызывается только для актеров, прикрепленных к Pawn'у
    // Поэтому вызываем её через правила
    // Игрок умер, запускаем таймер и начинаем отсчет до респавна
    function PawnBaseDied()
    {
        // Если спавн игроков во время волны с боссом запрещён, то информируем об этом игрока
        if(!Mut.bAllowSpawnOnBossWave && KFGT.bWaveBossInProgress)
            OwnerPC.ClientMessage(Mut.MSG_RespawnOnBossWave);
        else
            GoToState('RespawnTimer');
    }
    
    function ManageRespawn()
    {
        local array<Pawn> AlivePlayers;
        local bool bLiveToDeathTreshold;
        // Если игра закончилась, то делать нам тут больше нечего
        if(KFGT==None || KFGT.bGameEnded)
        {
            GoToState(''); // Отключаем таймер
            Return; // И выходим
        }
        if(OwnerPC!=None && OwnerPC.PlayerReplicationInfo!=None)
        {
            // Если игрок был заспавнен ГТ, то делать нам тут больше нечего
            if(OwnerPC.Pawn!=None)
            {
                RespawnCounter=0; // Обнуляем счетчик
                GoToState(''); // Отключаем таймер
                Return; // И выходим
            }
            // Если игрок перешел в наблюдатели, то делать нам тут больше нечего
            if(OwnerPC.PlayerReplicationInfo.bOnlySpectator)
            {
                if(RespawnCounter>0) RespawnCounter=0; // Обнуляем счетчик
                Return; // И выходим
            }
            // Проверяем, возможен ли респавн во время волны
            Class'AutoRespawnerUtilities'.Static.GetAlivePlayersList(Level, OwnerPC, AlivePlayers); // Получаем кол-во живых игроков
            // Отношение живых игроков к общему числу игроков, пороговое соотношение в зависимости от волны
            if(float(AlivePlayers.Length)/KFGT.NumPlayers<Mut.GetRespawnLiveToDeathThreshold(KFGT.WaveNum))
                bLiveToDeathTreshold=True;
            else
                bLiveToDeathTreshold=False;
            // Респавн не возможен, так как кол-во мертвых игроков слишком велико, информируем игрока о том, что респавн отключен
            if(bLiveToDeathTreshold)
            {
                if(LiveToDeathThresholdMsgTimeNext<Level.TimeSeconds && KFGT.bWaveInProgress)
                {
                    LiveToDeathThresholdMsgTimeNext=Level.TimeSeconds+Mut.LiveToDeathThresholdMsgTime;
                    KFGT.Broadcast(Self,Mut.MSG_RespawnThreshold);
                    if(RespawnCounter>0) RespawnCounter=0; // Обнуляем счетчик
                    Return; // И выходим
                }
            }
            else
                LiveToDeathThresholdMsgTimeNext=0;
            // Респавн возможен, проверяем, пора ли нам спавнится, если нет, то спамим сообщения с кол-вом оставшегося времени до респавна
            if(!bLiveToDeathTreshold && KFGT.bWaveInProgress)
            {
                if(RespawnCounter>Mut.RespawnTimeout)
                {
                    // Нужна ли здесь проверка на bOutOfLives?
                    if(OwnerPC.PlayerReplicationInfo.bOutOfLives)
                        Mut.SpawnWaitingPlayer(OwnerPC); // Пришло время спавнить игрока
                    RespawnCounter=0; // Обнуляем счетчик
                    GoToState(''); // Отключаем таймер
                }
                else
                    ShowWaitToRespawnMsg(); // Информируем игрока о том, что респавн через %time% секунд
            }
        } 
    }
    
    // Сообщение для игрока
    function ShowWaitToRespawnMsg()
    {
        local int RespawnDelay;
        RespawnDelay=Mut.RespawnTimeout-RespawnCounter; // Время до респавна
        RespawnCounter++; // Увеличиваем счетчик
        if(RespawnMsgDelay<=Level.TimeSeconds)
        {
            if(RespawnDelay>60)
            {
                OwnerPC.ClientMessage(Repl(Mut.MSG_WaitToRespawn,"%time%",RespawnDelay));
                RespawnMsgDelay=Level.TimeSeconds+30.0;
            }
            else if(Class'AutoRespawnerUtilities'.Static.Between(RespawnDelay, 30, 60))
            {
                OwnerPC.ClientMessage(Repl(Mut.MSG_WaitToRespawn,"%time%",RespawnDelay));
                RespawnMsgDelay=Level.TimeSeconds+10.0;
            }
            else if(Class'AutoRespawnerUtilities'.Static.Between(RespawnDelay, 10, 30))
            {
                OwnerPC.ClientMessage(Repl(Mut.MSG_WaitToRespawn,"%time%",RespawnDelay));
                RespawnMsgDelay=Level.TimeSeconds+5.0;
            }
            else if(Class'AutoRespawnerUtilities'.Static.Between(RespawnDelay, 0, 10))
            {
                OwnerPC.ClientMessage(Repl(Mut.MSG_WaitToRespawn,"%time%",RespawnDelay));
                RespawnMsgDelay=Level.TimeSeconds+1.0;
            }
        }
    }
    
    // При использовании стандартного Timer'a появляются задержки при выводе сообщений
    // Поэтому будем использовать State
    State RespawnTimer
    {
    Begin:
        While(True)
        {
            Sleep(1.0);
            ManageRespawn();
        }
    }
    
    defaultproperties
    {
        bAlwaysRelevant=True
        bHidden=True
    }

    Код:
    // Written by Essence
    // Утилита, отвечающая за неуязвимость при респавне
    class AutoRespawnerItem extends Actor;
    
    var AutoRespawnerMut Mut; // Ссылка на мут
    var int MortalityCounter; // Счетчик неуязвимости
    var PlayerController OwnerPC; // PlayerController владельца
    
    function PostBeginPlay()
    {
        OwnerPC=PlayerController(Pawn(Owner).Controller); // Запоминаем владельца
    }
    
    function ManageMortality()
    {
        if(OwnerPC!=None)
        {
            if(MortalityCounter==0)
                OwnerPC.bGodMode=True; // Выставляем неуязвимость
            MortalityCounter++; // Увеличиваем счетчик
            // Если счетчик достиг лимита, то вырубаем неуязвимость и уничтожаем предмет
            if(MortalityCounter>=Mut.UnbreakableTime)
            {
                OwnerPC.bGodMode=False;
                MortalityCounter=0;
                Destroy();
            }
        }
        else
            Destroy();
    }
    
    Auto State MortalityTimer
    {
    Begin:
        While(True)
        {
            Sleep(1.0);
            ManageMortality();
        }
    }
    
    defaultproperties
    {
        bAlwaysRelevant=True
        bHidden=True
    }

    Код:
    // Written by Dave_Scream
    // Modified by Essence
    // Утилита для хранения функций
    class AutoRespawnerUtilities extends Actor
        abstract;
    
    // Информация о кандидате
    struct BestPlayerInfo
    {
        var Pawn Saviour;
        var int PerkLevel;
        var int Status;
    };
    
    // Отлавливаем живых игроков
    static function GetAlivePlayersList(LevelInfo Level, PlayerController PC, out array<Pawn> PendingSaviours)
    {
        local Controller C;
        for(C=Level.ControllerList; C!=None; C=C.nextController)
        {
            if    (
                    C.bIsPlayer && // Контроллер принадлежит игроку
                    C.Pawn!=None && // Игрок жив (есть тело)
                    C.Pawn.Health>0 && // Хп игрока больше нуля
                    PlayerController(C)!=PC // Исключаем себя
                )
            {
                PendingSaviours[PendingSaviours.Length]=C.Pawn; // Заносим в массив
            }
        }
    }
    
    static function Pawn GetBestSaviour(LevelInfo Level, byte SpawnType, PlayerController PC)
    {
        local array<Pawn> Saviours;
        GetAlivePlayersList(Level, PC, Saviours); // Получаем список живых игроков
        if(SpawnType==3)
            Return GetBestSaviourByLevel(Saviours);
        else if(SpawnType==2)
            Return GetBestSaviourByStatus(Saviours);
        else // if(SpawnType==1)
            Return GetRandBestSaviour(Saviours);
    }
    
    static function Pawn GetBestSaviourByLevel(array<Pawn> Saviours)
    {
        local KFPlayerReplicationInfo KFPRI;
        local BestPlayerInfo BestPlayer;
        local int SaviourPerkLevel;
        local int i;
        // Бегаем по списку живых игроков
        for(i=0; i<Saviours.Length; i++)
        {
            KFPRI=KFPlayerReplicationInfo(Saviours[i].PlayerReplicationInfo);
            if(KFPRI!=None)
            {
                SaviourPerkLevel=KFPRI.ClientVeteranSkillLevel; // Уровень потенциального спасителя
                // Выбираем наилучший вариант
                if(SaviourPerkLevel>=BestPlayer.PerkLevel)
                {
                    // Если уровни совпали, шанс смены спасителя 50/50
                    if(KFPRI.ClientVeteranSkillLevel==SaviourPerkLevel && FRand()<0.5)
                    {
                        // Ничего не делаем
                    }
                    else
                    {
                        BestPlayer.PerkLevel=SaviourPerkLevel;
                        BestPlayer.Saviour=Saviours[i];
                    }
                }
            }
        }
        Return BestPlayer.Saviour; // Возвращаем спасителя
    }
    
    static function Pawn GetBestSaviourByStatus(array<Pawn> Saviours)
    {
        local BestPlayerInfo BestPlayer;
        local int SaviourStatus;
        local int i;
        // Бегаем по списку живых игроков
        for(i=0; i<Saviours.Length; i++)
        {
            SaviourStatus=Saviours[i].Health+Saviours[i].ShieldStrength; // Статус потенциального спасителя
            // Выбираем наилучший вариант
            if(SaviourStatus>BestPlayer.Status)
            {
                BestPlayer.Status=SaviourStatus;
                BestPlayer.Saviour=Saviours[i];
            }
        }
        Return BestPlayer.Saviour; // Возвращаем спасителя
    }
    
    static function Pawn GetRandBestSaviour(array<Pawn> Saviours)
    {
        local int MySavior;
        MySavior=Rand(Saviours.Length); // Выбираем рандомного игрока
        Return Saviours[MySavior]; // Возвращаем спасителя
    }
    
    // Математика
    static function bool Between(float a, float b, float c)
    {
        Return (a>b && a<=c);
    }

    Скачать:
    Ссылка
     
    Последнее редактирование: 15 мар 2020
    Hemoglobin, RaideN-, Flame и ещё 1-му нравится это.
  2. Essence

    Essence Moderator Команда форума

    Изменена реализация неуязвимости в связи с замечанием от Flame
    Шапка темы обновлена.
     
    RaideN- и Flame нравится это.