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.
459 lines
18 KiB
459 lines
18 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 extends Feature |
|
config(AcediaFixes); |
|
|
|
/** |
|
* 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 const 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.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.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(); |
|
} |
|
} |
|
|
|
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 |
|
{ |
|
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') |
|
// Listeners |
|
requiredListeners(0) = class'MutatorListener_FixDualiesCost' |
|
} |