/**
 *      This feature fixes lags caused by a zed time that can occur
 *  on some maps when a lot of zeds are present at once.
 *      As a side effect it also fixes an issue where during zed time speed up
 *  `zedTimeSlomoScale` was assumed to be default value of `0.2`.
 *  Now zed time will behave correctly with mods that
 *  change `zedTimeSlomoScale`.
 *      Copyright 2020 Anton Tarasenko
 *------------------------------------------------------------------------------
 * This file is part of Acedia.
 *
 * Acedia is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License, or
 * (at your option) any later version.
 *
 * Acedia is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Acedia.  If not, see <https://www.gnu.org/licenses/>.
 */
class FixZedTimeLags_Feature extends Feature
    dependson(ConnectionService);

/**
 *      When zed time activates, game speed is immediately set to
 *  `zedTimeSlomoScale` (0.2 by default), defined, like all other variables,
 *  in `KFGameType`. Zed time lasts `zedTimeDuration` seconds (3.0 by default),
 *  but during last `zedTimeDuration * 0.166` seconds (by default 0.498)
 *  it starts to speed back up, causing game speed to update every tick.
 *      This makes animations look more smooth when exiting zed-time;
 *  however, updating speed every tick for that purpose seems like
 *  an overkill and, combined with things like
 *  increased tick rate, certain maps and raised zed limit,
 *  it can lead to noticeable lags at the end of zed time.
 *      To fix this issue we disable `Tick()` event in
 *  `KFGameType` and then repeat that functionality in our own `Tick()` event,
 *  but only perform game speed updates occasionally,
 *  to make sure that overall amount of updates won't go over a limit,
 *  that can be configured via `maxGameSpeedUpdatesAmount`
 *      Author's test (looking really hard on clots' animations)
 *  seem to suggest that there shouldn't be much visible difference if
 *  we limit game speed updates to about 2 or 3.
 */

//      Max amount of game speed updates during speed up phase
//  (actual amount of updates can't be larger than amount of ticks).
//  On servers with default 30 tick rate there's usually
//  about 13 updates total on vanilla game.
//      Values lower than 1 are treated like 1.
var private config int maxGameSpeedUpdatesAmount;
//      [ADVANCED] Don't change this setting unless you know what you're doing.
//      Compatibility setting that allows to keep `GameInfo`'s `Tick()` event
//  from being disabled.
//  Useful when running Acedia along with custom `GameInfo`
//  (that isn't `KFGameType`) that relies on `Tick()` event.
//      Note, however, that in order to keep this fix working properly,
//  it's on you to make sure `KFGameType.Tick()` logic isn't executed.
var private config bool disableTick;
//  Counts how much time is left until next update
var private float updateCooldown;

protected function OnEnabled()
{
    if (disableTick) {
        _server.unreal.GetGameType().Disable('Tick');
    }
    _server.unreal.OnTick(self).connect = Tick;
}

protected function OnDisabled()
{
    if (disableTick) {
        _server.unreal.GetGameType().Enable('Tick');
    }
    _server.unreal.OnTick(self).Disconnect();
}

protected function SwapConfig(FeatureConfig config)
{
    local FixZedTimeLags newConfig;
    newConfig = FixZedTimeLags(config);
    if (newConfig == none) {
        return;
    }
    maxGameSpeedUpdatesAmount   = newConfig.maxGameSpeedUpdatesAmount;
    disableTick                 = newConfig.disableTick;
}

private function Tick(float delta, float timeDilationCoefficient)
{
    local KFGameType    gameType;
    local float         trueTimePassed;
    gameType = _server.unreal.GetKFGameType();
    if (!gameType.bZEDTimeActive)   return;
    //      Unfortunately we need to keep disabling `Tick()` probe function,
    //  because it constantly gets enabled back and I don't know where
    //  (maybe native code?); only really matters during zed time.
    if (disableTick) {
        gameType.Disable('Tick');
    }
    //  How much real (not in-game) time has passed
    trueTimePassed = delta / timeDilationCoefficient;
    gameType.currentZEDTimeDuration -= trueTimePassed;

    //  Handle speeding up phase
    if (gameType.bSpeedingBackUp) {
        DoSpeedBackUp(trueTimePassed, gameType);
    }
    else if (gameType.currentZEDTimeDuration < GetSpeedupDuration(gameType))
    {
        gameType.bSpeedingBackUp    = true;
        updateCooldown              = GetFullUpdateCooldown(gameType);
        TellClientsZedTimeEnds();
        DoSpeedBackUp(trueTimePassed, gameType);
    }
    //  End zed time once it's duration has passed
    if (gameType.currentZEDTimeDuration <= 0)
    {
        gameType.bZEDTimeActive         = false;
        gameType.bSpeedingBackUp        = false;
        gameType.zedTimeExtensionsUsed  = 0;
        gameType.SetGameSpeed(1.0);
    }
}

private final function TellClientsZedTimeEnds()
{
    local int                                   i;
    local KFPlayerController                    player;
    local ConnectionService                     service;
    local array<ConnectionService.Connection>   connections;
    service = ConnectionService(class'ConnectionService'.static.GetInstance());
    if (service == none) return;
    connections = service.GetActiveConnections();
    for (i = 0; i < connections.length; i += 1)
    {
        player = KFPlayerController(connections[i].controllerReference);
        if (player != none)
        {
            //  Play sound of leaving zed time
            player.ClientExitZedTime();
        }
    }
}

//      This function is called every tick during speed up phase and manages
//  gradual game speed increase.
private final function DoSpeedBackUp(float trueTimePassed, KFGameType gameType)
{
    //      Game speed will always be updated in our `Tick()` event
    //  at the very end of the zed time.
    //      The rest of the updates will be uniformly distributed
    //  over the speed up duration.

    local float newGameSpeed;
    local float slowdownScale;
    if (maxGameSpeedUpdatesAmount <= 1) return;
    if (updateCooldown > 0.0)
    {
        updateCooldown -= trueTimePassed;
        return;
    }
    else {
        updateCooldown = GetFullUpdateCooldown(gameType);
    }
    slowdownScale   =
        gameType.currentZEDTimeDuration / GetSpeedupDuration(gameType);
    newGameSpeed    = Lerp(slowdownScale, 1.0, gameType.zedTimeSlomoScale);
    gameType.SetGameSpeed(newGameSpeed);
}

private final function float GetSpeedupDuration(KFGameType gameType)
{
    return gameType.zedTimeDuration  * 0.166;
}

private final function float GetFullUpdateCooldown(KFGameType gameType)
{
    return GetSpeedupDuration(gameType) / maxGameSpeedUpdatesAmount;
}

defaultproperties
{
    configClass = class'FixZedTimeLags'
    maxGameSpeedUpdatesAmount   = 3
    disableTick                 = true
}