/**
 *      This feature addressed two inventory issues:
 *      1.  Players carrying amount of weapons that shouldn't be allowed by the
 *      weight limit.
 *      2.  Players carrying two variants of the same gun.
 *      For example carrying both M32 and camo M32.
 *      Single and dual version of the same weapon are also considered
 *      the same gun, so you can't carry both MK23 and dual MK23 or
 *      dual handcannons and golden handcannon.
 *
 *      It fixes them by doing repeated checks to find violations of those rules
 *      and destroys all droppable weapons of people that use this exploit.
 *      Copyright 2020 - 2021 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 FixInventoryAbuse_Feature extends Feature;

var private Timer checkTimer;

//      How often (in seconds) should we do our inventory validations?
//      We shouldn't really worry about performance, but there's also no need to
//  do this check too often.
var private /*config*/ float checkInterval;

struct DualiesPair
{
    var class<KFWeaponPickup>   single;
    var class<KFWeaponPickup>   dual;
};
//      For this fix to properly work, this array must contain an entry for
//  every dual weapon in the game (like pistols, with single and dual versions).
//  It's made configurable in case of custom dual weapons.
var private /*config*/ array<DualiesPair> dualiesClasses;

protected function OnEnabled()
{
    local float actualInterval;
    actualInterval = checkInterval;
    if (actualInterval <= 0) {
        actualInterval = 0.25;
    }
    checkTimer = _server.time.StartTimer(actualInterval, true);
    checkTimer.OnElapsed(self).connect = Timer;
}

protected function OnDisabled()
{
    _.memory.Free(checkTimer);
}

protected function SwapConfig(FeatureConfig config)
{
    local FixInventoryAbuse newConfig;
    newConfig = FixInventoryAbuse(config);
    if (newConfig == none) {
        return;
    }
    checkInterval   = newConfig.checkInterval;
    dualiesClasses  = newConfig.dualiesClasses;
    if (checkTimer != none) {
        checkTimer.SetInterval(checkInterval);
    }
}

//  Did player with this controller contribute to the latest dosh generation?
private final function bool IsWeightLimitViolated(KFHumanPawn playerPawn)
{
    if (playerPawn == none) return false;
    return (playerPawn.currentWeight > playerPawn.maxCarryWeight);
}

//      Returns a root pickup class.
//  For non-dual weapons, root class is defined as either:
//      1. the first variant (reskin), if there are variants for that weapon;
//      2. and as the class itself, if there are no variants.
//  For dual weapons (all dual pistols) root class is defined as
//  a root of their single version.
//      This definition is useful because:
//      ~ Vanilla game rules are such that player can only have two weapons
//      in the inventory if they have different roots;
//      ~ Root is easy to find.
private final function class<KFWeaponPickup> GetRootPickupClass(KFWeapon weapon)
{
    local int                   i;
    local class<KFWeaponPickup> root;
    if (weapon == none) return none;
    //  Start with a pickup of the given weapons
    root = class<KFWeaponPickup>(weapon.default.pickupClass);
    if (root == none)   return none;

    //      In case it's a dual version - find corresponding single pickup class
    //  (it's root would be the same).
    for (i = 0; i < dualiesClasses.length; i += 1)
    {
        if (dualiesClasses[i].dual == root)
        {
            root = dualiesClasses[i].single;
            break;
        }
    }
    //      Take either first variant class or the class itself, -
    //  it's going to be root by definition.
    if (root.default.variantClasses.length > 0)
    {
        root = class<KFWeaponPickup>(root.default.variantClasses[0]);
    }
    return root;
}

//      Returns `true` if passed pawn has two weapons that are just variants of
//  each other (they have the same root, see `GetRootPickupClass()`).
private final function bool HasDuplicateGuns(KFHumanPawn playerPawn)
{
    local int                       i, j;
    local Inventory                 inv;
    local KFWeapon                  nextWeapon;
    local class<KFWeaponPickup>     rootClass;
    local array< class<Pickup> >    rootList;
    if (playerPawn == none) return false;

    //  First find a root for every weapon in the pawn's inventory.
    for (inv = playerPawn.inventory; inv != none; inv = inv.inventory)
    {
        nextWeapon = KFWeapon(inv);
        if (nextWeapon == none)         continue;
        if (nextWeapon.bKFNeverThrow)   continue;
        rootClass = GetRootPickupClass(nextWeapon);
        if (rootClass != none) {
            rootList[rootList.length] = rootClass;
        }
    }
    //  Then just check obtained roots for duplicates.
    for (i = 0; i < rootList.length; i += 1)
    {
        for (j = i + 1; j < rootList.length; j += 1)
        {
            if (rootList[i] == rootList[j]) {
                return true;
            }
        }
    }
    return false;
}

private final function Vector DropWeapon(KFWeapon weaponToDrop)
{
    local Vector        x, y, z;
    local Vector        weaponVelocity;
    local Vector        dropLocation;
    local KFHumanPawn   playerPawn;
    if (weaponToDrop == none)   return Vect(0, 0, 0);
    playerPawn = KFHumanPawn(weaponToDrop.instigator);
    if (playerPawn == none)     return Vect(0, 0, 0);

    //  Calculations from `PlayerController.ServerThrowWeapon()`
    weaponVelocity = Vector(playerPawn.GetViewRotation());
    weaponVelocity *= (playerPawn.velocity dot weaponVelocity) + 150;
    weaponVelocity += Vect(0, 0, 100);
    //  Calculations from `Pawn.TossWeapon()`
    GetAxes(playerPawn.rotation, x, y, z);
    dropLocation = playerPawn.location + 0.8 * playerPawn.collisionRadius * x -
        0.5 * playerPawn.collisionRadius * y;
    //  Do the drop
    weaponToDrop.velocity = weaponVelocity;
    weaponToDrop.DropFrom(dropLocation);
}

//  Kill the gun devil!
private final function DropEverything(KFHumanPawn playerPawn)
{
    local int               i;
    local Inventory         inv;
    local KFWeapon          nextWeapon;
    local array<KFWeapon>   weaponList;
    if (playerPawn == none) return;
    //      Going through the linked list while removing items can be tricky,
    //  so just find all weapons first.
    for (inv = playerPawn.inventory; inv != none; inv = inv.inventory)
    {
        nextWeapon = KFWeapon(inv);
        if (nextWeapon == none)         continue;
        if (nextWeapon.bKFNeverThrow)   continue;
        weaponList[weaponList.length] = nextWeapon;
    }
    //  And destroy them later.
    for(i = 0; i < weaponList.length; i += 1) {
        DropWeapon(weaponList[i]);
    }
}

private function Timer(Timer source)
{
    local int                                   i;
    local KFHumanPawn                           nextPawn;
    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)
    {
        nextPawn = none;
        if (connections[i].controllerReference != none) {
            nextPawn = KFHumanPawn(connections[i].controllerReference.pawn);
        }
        if (IsWeightLimitViolated(nextPawn) || HasDuplicateGuns(nextPawn)) {
            DropEverything(nextPawn);
        }
    }
}

defaultproperties
{
    configClass = class'FixInventoryAbuse'
    checkInterval = 0.25
    dualiesClasses(0)=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup')
    dualiesClasses(1)=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup')
    dualiesClasses(2)=(single=class'KFMod.MK23Pickup',dual=class'KFMod.DualMK23Pickup')
    dualiesClasses(3)=(single=class'KFMod.DeaglePickup',dual=class'KFMod.DualDeaglePickup')
    dualiesClasses(4)=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDualDeaglePickup')
    dualiesClasses(5)=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup')
}