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.
 

485 lines
19 KiB

/**
* This feature fixes several issues related to the selling price of both
* single and dual pistols, all originating from the existence of dual weapons.
* Most notable issue is the ability to "print" money by buying and
* selling pistols in a certain way.
*
* It fixes all of the issues by manually setting pistols'
* `SellValue` variables to proper values.
* Fix only works with vanilla pistols, as it's unpredictable what
* custom ones can do and they can handle these issues on their own
* in a better way.
* 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 FixDualiesCost_Feature extends Feature;
/**
* Issues with pistols' cost may look varied and surface in
* a plethora of ways, but all of them originate from the two main errors
* in vanilla's code:
* 1. If you have a pistol in your inventory at the time when you
* buy/pickup another one - the sell value of resulting dualies is
* incorrectly set to the sell value of the second pistol;
* 2. When player has dual pistols and drops one on the floor, -
* the sell value for the one left with the player isn't set.
* All weapons in Killing Floor get sell value assigned to them
* (appropriately, in a `SellValue` variable). This is to ensure that the sell
* price is set the moment players buys the gun. Otherwise, due to ridiculous
* perked discounts, you'd be able to buy a pistol at 30% price
* as sharpshooter, but sell at 75% of a price as any other perk,
* resulting in 45% of pure profit.
* Unfortunately, that's exactly what happens when `SellValue` isn't set
* (left as it's default value of `-1`): sell value of such weapons is
* determined only at the moment of sale and depends on the perk of the seller,
* allowing for possible exploits.
*
* These issues are fixed by directly assigning
* proper values to `SellValue`. To do that we need to detect when player
* buys/sells/drops/picks up weapons, which we accomplish by catching
* `CheckReplacement()` event for weapon instances. This approach has two
* issues.
* One is that, if vanilla's code sets an incorrect sell value, -
* it's doing it after weapon is spawned and, therefore,
* after `CheckReplacement()` call, so we have, instead, to remember to do
* it later, as early as possible
* (either the next tick or before another operation with weapons).
* Another issue is that when you have a pistol and pick up a pistol of
* the same type, - at the moment dualies instance is spawned,
* the original pistol in player's inventory is gone and we can't use
* it's sell value to calculate new value of dual pistols.
* This problem is solved by separately recording the value for every
* single pistol every tick.
* However, if pistol pickups are placed close enough together on the map,
* player can start touching them (which triggers a pickup) at the same time,
* picking them both in a single tick. This leaves us no room to record
* the value of a single pistol players picks up first.
* To get it we use game rules to catch `OverridePickupQuery` event that's
* called before the first one gets destroyed,
* but after it's sell value was already set.
* Last issue is that when player picks up a second pistol - we don't know
* it's sell value and, therefore, can't calculate value of dual pistols.
* This is resolved by recording that value directly from a pickup,
* in abovementioned function `OverridePickupQuery`.
* NOTE: 9mm is an exception due to the fact that you always have at least
* one and the last one can't be sold. We'll deal with it by setting
* the following rule: sell value of the un-droppable pistol is always 0
* and the value of a pair of 9mms is the value of the single droppable pistol.
*/
// Some issues involve possible decrease in pistols' price and
// don't lead to exploit, but are still bugs and require fixing.
// If you have a Deagle in your inventory and then get another one
// (by either buying or picking it off the ground) - the price of resulting
// dual pistols will be set to the price of the last deagle,
// like the first one wasn't worth anything at all.
// In particular this means that (prices are off-perk for more clarity):
// 1. If you buy dual deagles (-1000 do$h) and then sell them at 75% of
// the cost (+750 do$h), you lose 250 do$h;
// 2. If you first buy a deagle (-500 do$h), then buy
// the second one (-500 do$h) and then sell them, you'll only get
// 75% of the cost of 1 deagle (+375 do$h), now losing 625 do$h;
// 3. So if you already have bought a deagle (-500 do$h),
// you can get a more expensive weapon by doing a stupid thing
// and first selling your Deagle (+375 do$h),
// then buying dual deagles (-1000 do$h).
// If you sell them after that, you'll gain 75% of the cost of
// dual deagles (+750 do$h), leaving you with losing only 375 do$h.
// Of course, situations described above are only relevant if you're planning
// to sell your weapons at some point and most people won't even notice it.
// But such an oversight still shouldn't exist in a game and we fix it by
// setting sell value of dualies as a sum of values of each pistol.
// Yet, fixing this issue leads to players having more expensive
// (while fairly priced) weapons than on vanilla, technically making
// the game easier. And some people might object to having that in
// a whitelisted bug-fixing feature.
// These people are, without a question, complete degenerates.
// But making mods for only non-mentally challenged isn't inclusive.
// So we add this option.
// Set it to `false` if you only want to fix ammo printing
// and leave the rest of the bullshit as-is.
var private /*config*/ bool allowSellValueIncrease;
// Describe all the possible pairs of dual pistols in a vanilla game.
struct DualiesPair
{
var class<KFWeapon> single;
var class<KFWeapon> dual;
};
var private const array<DualiesPair> dualiesClasses;
// Describe sell values that need to be applied at earliest later point.
struct WeaponValuePair
{
// Reference to `KFWeapon` instance
var NativeActorRef weapon;
var float value;
};
var private const array<WeaponValuePair> pendingValues;
// Describe sell values of all currently existing single pistols.
struct WeaponDataRecord
{
// Reference to `KFWeapon` instance
var NativeActorRef reference;
var class<KFWeapon> class;
var float value;
// The whole point of this structure is to remember value of a weapon
// after it's destroyed. Since `reference` will become `none` by then,
// we will use the `owner` reference to identify the weapon.
// Reference to `Pawn`.
var NativeActorRef owner;
};
var private const array<WeaponDataRecord> storedValues;
// Sell value of the last seen pickup in `OverridePickupQuery`
var private int nextSellValue;
protected function OnEnabled()
{
local LevelInfo level;
local KFWeapon nextWeapon;
_.unreal.OnTick(self).connect = Tick;
_.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement;
_.unreal.gameRules.OnOverridePickupQuery(self).connect = PickupQuery;
level = _.unreal.GetLevel();
// Find all weapons, that spawned when this fix wasn't running.
foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon) {
RegisterSinglePistol(nextWeapon, false);
}
}
protected function OnDisabled()
{
local int i;
_.unreal.OnTick(self).Disconnect();
_.unreal.mutator.OnCheckReplacement(self).Disconnect();
_.unreal.gameRules.OnOverridePickupQuery(self).Disconnect();
for (i = 0; i < storedValues.length; i += 1)
{
storedValues[i].reference.FreeSelf();
storedValues[i].owner.FreeSelf();
}
for (i = 0; i < pendingValues.length; i += 1) {
pendingValues[i].weapon.FreeSelf();
}
}
protected function SwapConfig(FeatureConfig config)
{
local FixDualiesCost newConfig;
newConfig = FixDualiesCost(config);
if (newConfig == none) {
return;
}
allowSellValueIncrease = newConfig.allowSellValueIncrease;
}
private final function bool CheckReplacement(
Actor other,
out byte isSuperRelevant)
{
local KFWeapon weapon;
weapon = KFWeapon(other);
if (weapon == none) {
return true;
}
RegisterSinglePistol(weapon, true);
FixCostAfterThrow(weapon);
FixCostAfterBuying(weapon);
FixCostAfterPickUp(weapon);
return true;
}
function bool PickupQuery(
Pawn other,
Pickup item,
out byte allowPickup)
{
local KFWeaponPickup weaponPickup;
weaponPickup = KFWeaponPickup(item);
if (weaponPickup != none)
{
ApplyPendingValues();
StoreSinglePistolValues();
nextSellValue = weaponPickup.sellValue;
}
return false;
}
// Finds a weapon of a given class in given `Pawn`'s inventory.
// Returns `none` if weapon isn't there.
private final function KFWeapon GetWeaponOfClass(
Pawn playerPawn,
class<KFWeapon> weaponClass
)
{
local Inventory invIter;
if (playerPawn == none) return none;
invIter = playerPawn.inventory;
while (invIter != none)
{
if (invIter.class == weaponClass) {
return KFWeapon(invIter);
}
invIter = invIter.inventory;
}
return none;
}
// Gets weapon index in our record of dual pistol classes.
// Second variable determines whether we're searching for single
// or dual variant:
// ~ `true` - searching for single
// ~ `false` - for dual
// Returns `-1` if weapon isn't found
// (dual MK23 won't be found as a single weapon).
private final function int GetIndexAs(KFWeapon weapon, bool asSingle)
{
local int i;
if (weapon == none) return -1;
for (i = 0; i < dualiesClasses.length; i += 1)
{
if (asSingle && dualiesClasses[i].single == weapon.class) {
return i;
}
if (!asSingle && dualiesClasses[i].dual == weapon.class) {
return i;
}
}
return -1;
}
// Calculates full cost of a weapon with a discount,
// dependent on it's instigator's perk.
private final function float GetFullCost(KFWeapon weapon)
{
local float cost;
local class<KFWeaponPickup> pickupClass;
local KFPlayerReplicationInfo instigatorRI;
if (weapon == none) return 0.0;
pickupClass = class<KFWeaponPickup>(weapon.default.pickupClass);
if (pickupClass == none) return 0.0;
cost = pickupClass.default.cost;
if (weapon.instigator != none)
{
instigatorRI =
KFPlayerReplicationInfo(weapon.instigator.playerReplicationInfo);
}
if (instigatorRI != none && instigatorRI.clientVeteranSkill != none)
{
cost *= instigatorRI.clientVeteranSkill.static
.GetCostScaling(instigatorRI, pickupClass);
}
return cost;
}
// If passed weapon is a pistol - we start tracking it's value;
// Otherwise - do nothing.
public final function RegisterSinglePistol(
KFWeapon singlePistol,
bool justSpawned
)
{
local WeaponDataRecord newRecord;
if (singlePistol == none) return;
if (GetIndexAs(singlePistol, true) < 0) return;
newRecord.reference = _.unreal.ActorRef(singlePistol);
newRecord.class = singlePistol.class;
newRecord.owner = _.unreal.ActorRef(singlePistol.instigator);
if (justSpawned) {
newRecord.value = nextSellValue;
}
else {
newRecord.value = singlePistol.sellValue;
}
storedValues[storedValues.length] = newRecord;
}
// Fixes sell value after player throws one pistol out of a pair.
public final function FixCostAfterThrow(KFWeapon singlePistol)
{
local int index;
local KFWeapon dualPistols;
if (singlePistol == none) return;
index = GetIndexAs(singlePistol, true);
if (index < 0) return;
dualPistols = GetWeaponOfClass( singlePistol.instigator,
dualiesClasses[index].dual);
if (dualPistols == none) return;
// Sell value recorded into `dualPistols` will end up as a value of
// a dropped pickup.
// Sell value of `singlePistol` will be the value for the pistol,
// left in player's hands.
if (dualPistols.class == class'KFMod.Single')
{
// 9mm is an exception.
// Remaining weapon costs nothing.
singlePistol.sellValue = 0;
// We don't change the sell value of the dropped weapon,
// as it's default behavior to transfer full value of a pair to it.
return;
}
// For other pistols - divide the value.
singlePistol.sellValue = dualPistols.sellValue / 2;
dualPistols.sellValue = singlePistol.sellValue;
}
// Fixes sell value after buying a pair of dual pistols,
// if player already had a single version.
public final function FixCostAfterBuying(KFWeapon dualPistols)
{
local int index;
local KFWeapon singlePistol;
local WeaponValuePair newPendingValue;
if (dualPistols == none) return;
index = GetIndexAs(dualPistols, false);
if (index < 0) return;
singlePistol = GetWeaponOfClass(dualPistols.instigator,
dualiesClasses[index].single);
if (singlePistol == none) return;
// `singlePistol` will get destroyed, so it's sell value is irrelevant.
// `dualPistols` will be the new pair of pistols, but it's value will
// get overwritten by vanilla's code after this function.
// So we must add it to pending values to be changed later.
newPendingValue.weapon = _.unreal.ActorRef(dualPistols);
if (dualPistols.class == class'KFMod.Dualies')
{
// 9mm is an exception.
// The value of pair of 9mms is the price of additional pistol,
// that defined as a price of a pair in game.
newPendingValue.value = GetFullCost(dualPistols) * 0.75;
}
else
{
// Otherwise price of a pair is the price of two pistols:
// `singlePistol.sellValue` - the one we had
// `(FullCost / 2) * 0.75` - and the one we bought
newPendingValue.value = singlePistol.sellValue
+ (GetFullCost(dualPistols) / 2) * 0.75;
}
pendingValues[pendingValues.length] = newPendingValue;
}
// Fixes sell value after player picks up a single pistol,
// while already having one of the same time in his inventory.
public final function FixCostAfterPickUp(KFWeapon dualPistols)
{
local int i;
local int index;
local KFWeapon singlePistol;
local WeaponValuePair newPendingValue;
if (dualPistols == none) return;
// In both cases of:
// 1. buying dualies, without having a single pistol of
// corresponding type;
// 2. picking up a second pistol, while having another one;
// by the time of `CheckReplacement()` (and, therefore, this function)
// is called, there's no longer any single pistol in player's inventory
// (in first case it never was there, in second - it got destroyed).
// To distinguish between those possibilities we can check the owner of
// the spawned weapon, since it's only set to instigator at the time of
// `CheckReplacement()` when player picks up a weapon.
// So we require that owner exists.
if (dualPistols.owner == none) return;
index = GetIndexAs(dualPistols, false);
if (index < 0) return;
singlePistol = GetWeaponOfClass(dualPistols.instigator,
dualiesClasses[index].single);
if (singlePistol != none) return;
if (nextSellValue == -1) {
nextSellValue = GetFullCost(dualPistols) * 0.75;
}
for (i = 0; i < storedValues.length; i += 1)
{
if (storedValues[i].reference.Get() != none) continue;
if (storedValues[i].class != dualiesClasses[index].single) continue;
if (storedValues[i].owner.Get() != dualPistols.instigator) continue;
newPendingValue.weapon = _.unreal.ActorRef(dualPistols);
newPendingValue.value = storedValues[i].value + nextSellValue;
pendingValues[pendingValues.length] = newPendingValue;
break;
}
}
private final function ApplyPendingValues()
{
local int i;
local KFWeapon nextWeapon;
for (i = 0; i < pendingValues.length; i += 1)
{
nextWeapon = KFWeapon(pendingValues[i].weapon.Get());
pendingValues[i].weapon.FreeSelf();
if (nextWeapon == none) {
continue;
}
// Our fixes can only increase the correct (`!= -1`)
// sell value of weapons, so if we only need to change sell value
// if we're allowed to increase it or it's incorrect.
if (allowSellValueIncrease || nextWeapon.sellValue == -1) {
nextWeapon.sellValue = pendingValues[i].value;
}
}
pendingValues.length = 0;
}
private final function StoreSinglePistolValues()
{
local int i;
local KFWeapon nextWeapon;
while (i < storedValues.length)
{
nextWeapon = KFWeapon(storedValues[i].reference.Get());
if (nextWeapon == none)
{
storedValues[i].reference.FreeSelf();
storedValues[i].owner.FreeSelf();
storedValues.Remove(i, 1);
continue;
}
storedValues[i].owner.Set(nextWeapon.instigator);
storedValues[i].value = nextWeapon.sellValue;
i += 1;
}
}
private function Tick(float delta, float timeDilationCoefficient)
{
ApplyPendingValues();
StoreSinglePistolValues();
}
defaultproperties
{
configClass = class'FixDualiesCost'
allowSellValueIncrease = true
// Inner variables
dualiesClasses(0)=(single=class'KFMod.Single',dual=class'KFMod.Dualies')
dualiesClasses(1)=(single=class'KFMod.Magnum44Pistol',dual=class'KFMod.Dual44Magnum')
dualiesClasses(2)=(single=class'KFMod.MK23Pistol',dual=class'KFMod.DualMK23Pistol')
dualiesClasses(3)=(single=class'KFMod.Deagle',dual=class'KFMod.DualDeagle')
dualiesClasses(4)=(single=class'KFMod.GoldenDeagle',dual=class'KFMod.GoldenDualDeagle')
dualiesClasses(5)=(single=class'KFMod.FlareRevolver',dual=class'KFMod.DualFlareRevolver')
}