Bug fixes for Killing Floor
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

245 lines
9.2 KiB

/**
* This feature addressed two dosh-related issues:
* 1. Crashing servers by spamming 'CashPickup' actors with 'TossCash';
* 2. Breaking collision detection logic by stacking large amount of
* 'CashPickup' actors in one place, which allows one to either
* reach unintended locations or even instantly kill zeds.
*
* It fixes them by limiting speed, with which dosh can spawn, and
* allowing this limit to decrease when there's already too much dosh
* present on the map.
* Copyright 2019 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 FixDoshSpam extends Feature
config(AcediaFixes);
/**
* First, we limit amount of dosh that can be spawned simultaneously.
* The simplest method is to place a cooldown on spawning 'CashPickup' actors,
* i.e. after spawning one 'CashPickup' we'd completely prevent spawning
* any other instances of it for a fixed amount of time.
* However, that might allow a malicious spammer to block others from
* throwing dosh, - all he needs to do is to spam dosh at right time intervals.
* We'll resolve this issue by recording how many 'CashPickup' actors
* each player has spawned as their "contribution" and decay
* that value with time, only allowing to spawn new dosh after
* contribution decayed to zero. Speed of decay is derived from current dosh
* spawning speed limit and decreases with amount of players
* with non-zero contributions (since it means that they're throwing dosh).
* Second issue is player amassing a large amount of dosh in one point
* that leads to skipping collision checks, which then allows players to pass
* through level geometry or enter zeds' collisions, instantly killing them.
* Since dosh disappears on it's own, the easiest method to prevent that is to
* severely limit how much dosh players can throw per second,
* so that there's never enough dosh laying around to affect collision logic.
* The downside to such severe limitations is that game behaves less
* vanilla-like, where you could throw away streams of dosh.
* To solve that we'll first use a more generous limit on dosh players can
* throw per second, but will track how much dosh is currently present
* in a level and linearly decelerate speed, according to that amount.
*/
// Highest and lowest speed with which players can throw dosh wads.
// It'll be evenly spread between all players.
// For example, if speed is set to 6 and only one player will be spamming dosh,
// - he'll be able to throw 6 wads of dosh per second;
// but if all 6 players are spamming it, - each will throw only 1 per second.
// NOTE: these speed values can be exceeded, since a player is guaranteed
// to be able to throw at least one wad of dosh, if he didn't do so in awhile.
// NOTE #2: if maximum value is less than minimum one,
// the lowest (maximum one) will be used.
var private config const float doshPerSecondLimitMax;
var private config const float doshPerSecondLimitMin;
// Amount of dosh pickups on the map at which we must set dosh per second
// to 'doshPerSecondLimitMin'.
// We use 'doshPerSecondLimitMax' when there's no dosh on the map and
// scale linearly between them as it's amount grows.
var private config const int criticalDoshAmount;
// To limit dosh spawning speed we need some measure of
// time passage between ticks.
// This variable stores last value seen by us as a good approximation.
// It's a real (not in-game) time.
var private float lastTickDuration;
// This structure records how much a certain player has
// contributed to an overall dosh creation.
struct DoshStreamPerPlayer
{
var PlayerController player;
// Amount of dosh we remember this player creating, decays with time.
var float contribution;
};
var private array<DoshStreamPerPlayer> currentContributors;
// Wads of cash that are lying around on the map.
var private array<CashPickup> wads;
protected function OnEnabled()
{
local CashPickup nextCash;
// Find all wads of cash laying around on the map,
// so that we could accordingly limit the cash spam.
foreach level.DynamicActors(class'KFMod.CashPickup', nextCash) {
wads[wads.length] = nextCash;
}
}
protected function OnDisabled()
{
wads.length = 0;
currentContributors.length = 0;
}
// Did player with this controller contribute to the latest dosh generation?
public final function bool IsContributor(PlayerController player)
{
return (GetContributorIndex(player) >= 0);
}
// Did we already reach allowed limit of dosh per second?
public final function bool IsDoshStreamOverLimit()
{
local int i;
local float overallContribution;
overallContribution = 0.0;
for (i = 0; i < currentContributors.length; i += 1) {
overallContribution += currentContributors[i].contribution;
}
return (overallContribution > lastTickDuration * GetCurrentDPSLimit());
}
// What is our current dosh per second limit?
private final function float GetCurrentDPSLimit()
{
local float speedScale;
if (doshPerSecondLimitMax < doshPerSecondLimitMin) {
return doshPerSecondLimitMax;
}
speedScale = Float(wads.length) / Float(criticalDoshAmount);
speedScale = FClamp(speedScale, 0.0, 1.0);
// At 0.0 scale (no dosh on the map) - use max speed
// At 1.0 scale (critical dosh on the map) - use min speed
return Lerp(speedScale, doshPerSecondLimitMax, doshPerSecondLimitMin);
}
// Returns index of the contributor corresponding to the given controller.
// Returns '-1' if no connection correspond to the given controller.
// Returns '-1' if given controller is equal to 'none'.
private final function int GetContributorIndex(PlayerController player)
{
local int i;
if (player == none) return -1;
for (i = 0; i < currentContributors.length; i += 1)
{
if (currentContributors[i].player == player) {
return i;
}
}
return -1;
}
// Adds given cash to given player contribution record and
// registers that cash in our wads array.
// Does nothing if given cash was already registered.
public final function AddContribution(PlayerController player, CashPickup cash)
{
local int i;
local int playerIndex;
local DoshStreamPerPlayer newStreamRecord;
// Check if given dosh was already accounted for.
for (i = 0; i < wads.length; i += 1)
{
if (cash == wads[i]) {
return;
}
}
wads[wads.length] = cash;
// Add contribution to player
playerIndex = GetContributorIndex(player);
if (playerIndex >= 0)
{
currentContributors[playerIndex].contribution += 1.0;
return;
}
newStreamRecord.player = player;
newStreamRecord.contribution = 1.0;
currentContributors[currentContributors.length] = newStreamRecord;
}
private final function ReducePlayerContributions(float trueTimePassed)
{
local int i;
local float streamReduction;
streamReduction = trueTimePassed *
(GetCurrentDPSLimit() / currentContributors.length);
for (i = 0; i < currentContributors.length; i += 1) {
currentContributors[i].contribution -= streamReduction;
}
}
// Clean out wads that disappeared or were picked up by players.
private final function CleanWadsArray()
{
local int i;
i = 0;
while (i < wads.length)
{
if (wads[i] == none) {
wads.Remove(i, 1);
}
else {
i += 1;
}
}
}
// Don't track players that no longer contribute to dosh generation.
private final function RemoveNonContributors()
{
local int i;
local array<DoshStreamPerPlayer> updContributors;
for (i = 0; i < currentContributors.length; i += 1)
{
// We want to keep on record even players that quit,
// since their contribution still must be accounted for.
if (currentContributors[i].contribution <= 0.0) continue;
updContributors[updContributors.length] = currentContributors[i];
}
currentContributors = updContributors;
}
event Tick(float delta)
{
local float trueTimePassed;
trueTimePassed = delta * (1.1 / level.timeDilation);
CleanWadsArray();
ReducePlayerContributions(trueTimePassed);
RemoveNonContributors();
lastTickDuration = trueTimePassed;
}
defaultproperties
{
doshPerSecondLimitMax = 50
doshPerSecondLimitMin = 5
criticalDoshAmount = 25
// Listeners
requiredListeners(0) = class'MutatorListener_FixDoshSpam'
}