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.
298 lines
11 KiB
298 lines
11 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 - 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 FixDoshSpam_Feature extends Feature; |
|
|
|
/** |
|
* 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*/ float doshPerSecondLimitMax; |
|
var private /*config*/ 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*/ int criticalDoshAmount; |
|
|
|
// We immediately reduce the rate dosh can be spawned at when players throw |
|
// new wads of cash. But, for performance reasons, we only periodically turn it |
|
// back up. This interval determines how often we check for whether it's okay |
|
// to raise the limit on the spawned dosh. |
|
// You should not set this value too high, it is recommended not to exceed |
|
// 1 second. |
|
var private /*config*/ float checkInterval; |
|
|
|
// This structure records how much a certain player has |
|
// contributed to an overall dosh creation. |
|
struct DoshStreamPerPlayer |
|
{ |
|
// Reference to `PlayerController` |
|
var NativeActorRef 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<NativeActorRef> wads; |
|
|
|
// Generates "reset" events when `wads` array is getting cleaned from |
|
// destroyed/picked up dosh and players' contributions are reduced. |
|
var private Timer checkTimer; |
|
|
|
protected function OnEnabled() |
|
{ |
|
local LevelInfo level; |
|
local CashPickup nextCash; |
|
checkTimer = _server.time.StartRealTimer(checkInterval, true); |
|
checkTimer.OnElapsed(self).connect = Tick; |
|
_server.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement; |
|
level = _server.unreal.GetLevel(); |
|
// 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] = _server.unreal.ActorRef(nextCash); |
|
} |
|
} |
|
|
|
protected function OnDisabled() |
|
{ |
|
local int i; |
|
_.memory.FreeMany(wads); |
|
for (i = 0; i < currentContributors.length; i += 1) { |
|
currentContributors[i].player.FreeSelf(); |
|
} |
|
wads.length = 0; |
|
currentContributors.length = 0; |
|
_server.unreal.mutator.OnCheckReplacement(self).Disconnect(); |
|
checkTimer.FreeSelf(); |
|
} |
|
|
|
protected function SwapConfig(FeatureConfig config) |
|
{ |
|
local FixDoshSpam newConfig; |
|
newConfig = FixDoshSpam(config); |
|
if (newConfig == none) { |
|
return; |
|
} |
|
doshPerSecondLimitMax = newConfig.doshPerSecondLimitMax; |
|
doshPerSecondLimitMin = newConfig.doshPerSecondLimitMin; |
|
criticalDoshAmount = newConfig.criticalDoshAmount; |
|
checkInterval = newConfig.checkInterval; |
|
if (checkTimer != none) { |
|
checkTimer.SetInterval(checkInterval); |
|
} |
|
} |
|
|
|
private final function bool CheckReplacement( |
|
Actor other, |
|
out byte isSuperRelevant) |
|
{ |
|
local PlayerController player; |
|
if (other == none) return true; |
|
if (other.class != class'CashPickup') return true; |
|
// This means this dosh wasn't spawned in `TossCash()` of `KFPawn`, |
|
// so it isn't related to the exploit we're trying to fix. |
|
if (other.instigator == none) return true; |
|
|
|
// We only want to prevent spawning cash if we're already over |
|
// the limit and the one trying to throw this cash contributed to it. |
|
// We allow other players to throw at least one wad of cash. |
|
player = PlayerController(other.instigator.controller); |
|
if (IsDoshStreamOverLimit() && IsContributor(player)) { |
|
return false; |
|
} |
|
// If we do spawn cash - record this contribution. |
|
AddContribution(player, CashPickup(other)); |
|
return true; |
|
} |
|
|
|
// 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; |
|
local float allowedContribution; |
|
overallContribution = 0.0; |
|
for (i = 0; i < currentContributors.length; i += 1) { |
|
overallContribution += currentContributors[i].contribution; |
|
} |
|
allowedContribution = checkTimer.GetElapsedTime() * GetCurrentDPSLimit(); |
|
return overallContribution > allowedContribution; |
|
} |
|
|
|
// 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.Get() == player) { |
|
return i; |
|
} |
|
} |
|
return -1; |
|
} |
|
|
|
// Adds given cash to given player contribution record and |
|
// registers that cash in our wads array. |
|
public final function AddContribution(PlayerController player, CashPickup cash) |
|
{ |
|
local int playerIndex; |
|
local DoshStreamPerPlayer newStreamRecord; |
|
wads[wads.length] = _server.unreal.ActorRef(cash); |
|
// Add contribution to player |
|
playerIndex = GetContributorIndex(player); |
|
if (playerIndex >= 0) |
|
{ |
|
currentContributors[playerIndex].contribution += 1.0; |
|
return; |
|
} |
|
newStreamRecord.player = _server.unreal.ActorRef(player); |
|
newStreamRecord.contribution = 1.0; |
|
currentContributors[currentContributors.length] = newStreamRecord; |
|
} |
|
|
|
private final function ReducePlayerContributions() |
|
{ |
|
local int i; |
|
local float streamReduction; |
|
streamReduction = checkInterval * |
|
(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].Get() == none) |
|
{ |
|
wads[i].FreeSelf(); |
|
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) { |
|
currentContributors[i].player.FreeSelf(); |
|
} |
|
else { |
|
updContributors[updContributors.length] = currentContributors[i]; |
|
} |
|
} |
|
currentContributors = updContributors; |
|
} |
|
|
|
private function Tick(Timer source) |
|
{ |
|
CleanWadsArray(); |
|
ReducePlayerContributions(); |
|
RemoveNonContributors(); |
|
} |
|
|
|
defaultproperties |
|
{ |
|
configClass = class'FixDoshSpam' |
|
doshPerSecondLimitMax = 50 |
|
doshPerSecondLimitMin = 5 |
|
criticalDoshAmount = 25 |
|
checkInterval = 0.25 |
|
} |