From 233a11df8915632ce8981d8c63c4edeb73ee322a Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 18 Feb 2020 20:22:09 +0700 Subject: [PATCH] Add `FixDualiesCost` feature 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. --- .../FixDualiesCost/DualiesCostRule.uc | 45 ++ .../Features/FixDualiesCost/FixDualiesCost.uc | 454 ++++++++++++++++++ .../MutatorListener_FixDualiesCost.uc | 43 ++ 3 files changed, 542 insertions(+) create mode 100644 sources/Features/FixDualiesCost/DualiesCostRule.uc create mode 100644 sources/Features/FixDualiesCost/FixDualiesCost.uc create mode 100644 sources/Features/FixDualiesCost/MutatorListener_FixDualiesCost.uc diff --git a/sources/Features/FixDualiesCost/DualiesCostRule.uc b/sources/Features/FixDualiesCost/DualiesCostRule.uc new file mode 100644 index 0000000..aa03156 --- /dev/null +++ b/sources/Features/FixDualiesCost/DualiesCostRule.uc @@ -0,0 +1,45 @@ +/** + * This rule detects any pickup events to allow us to + * properly record and/or fix pistols' prices. + * 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 . + */ +class DualiesCostRule extends GameRules; + +function bool OverridePickupQuery +( + Pawn other, + Pickup item, + out byte allowPickup +) +{ + local KFWeaponPickup weaponPickup; + local FixDualiesCost dualiesCostFix; + weaponPickup = KFWeaponPickup(item); + dualiesCostFix = FixDualiesCost(class'FixDualiesCost'.static.GetInstance()); + if (weaponPickup != none && dualiesCostFix != none) + { + dualiesCostFix.ApplyPendingValues(); + dualiesCostFix.StoreSinglePistolValues(); + dualiesCostFix.SetNextSellValue(weaponPickup.sellValue); + } + return super.OverridePickupQuery(other, item, allowPickup); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Features/FixDualiesCost/FixDualiesCost.uc b/sources/Features/FixDualiesCost/FixDualiesCost.uc new file mode 100644 index 0000000..2fd3822 --- /dev/null +++ b/sources/Features/FixDualiesCost/FixDualiesCost.uc @@ -0,0 +1,454 @@ +/** + * 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 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 . + */ +class FixDualiesCost 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 const bool allowSellValueIncrease; + +// Describe all the possible pairs of dual pistols in a vanilla game. +struct DualiesPair +{ + var class single; + var class dual; +}; +var private const array dualiesClasses; + +// Describe sell values that need to be applied at earliest later point. +struct WeaponValuePair +{ + var KFWeapon weapon; + var float value; +}; +var private const array pendingValues; + +// Describe sell values of all currently existing single pistols. +struct WeaponDataRecord +{ + var KFWeapon reference; + var class 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. + var Pawn owner; +}; +var private const array storedValues; + +// Sell value of the last seen pickup in 'OverridePickupQuery' +var private int nextSellValue; + +public function OnEnabled() +{ + local KFWeapon nextWeapon; + // Find all frags, that spawned when this fix wasn't running. + foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon) + { + RegisterSinglePistol(nextWeapon, false); + } + level.game.AddGameModifier(Spawn(class'DualiesCostRule')); +} + +public function OnDisabled() +{ + local GameRules rulesIter; + local DualiesCostRule ruleToDestroy; + // Check first rule + if (level.game.gameRulesModifiers == none) return; + + ruleToDestroy = DualiesCostRule(level.game.gameRulesModifiers); + if (ruleToDestroy != none) + { + level.game.gameRulesModifiers = ruleToDestroy.nextGameRules; + ruleToDestroy.Destroy(); + return; + } + // Check rest of the rules + rulesIter = level.game.gameRulesModifiers; + while (rulesIter != none) + { + ruleToDestroy = DualiesCostRule(rulesIter.nextGameRules); + if (ruleToDestroy != none) + { + rulesIter.nextGameRules = ruleToDestroy.nextGameRules; + ruleToDestroy.Destroy(); + } + rulesIter = rulesIter.nextGameRules; + } +} + +public final function SetNextSellValue(int newValue) +{ + nextSellValue = newValue; +} + +// 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 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 pickupClass; + local KFPlayerReplicationInfo instigatorRI; + if (weapon == none) return 0.0; + pickupClass = class(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 = singlePistol; + newRecord.class = singlePistol.class; + newRecord.owner = 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 = 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 != none) continue; + if (storedValues[i].class != dualiesClasses[index].single) continue; + if (storedValues[i].owner != dualPistols.instigator) continue; + newPendingValue.weapon = dualPistols; + newPendingValue.value = storedValues[i].value + nextSellValue; + pendingValues[pendingValues.length] = newPendingValue; + break; + } +} + +public final function ApplyPendingValues() +{ + local int i; + for (i = 0; i < pendingValues.length; i += 1) + { + if (pendingValues[i].weapon == 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 || pendingValues[i].weapon.sellValue == -1) + { + pendingValues[i].weapon.sellValue = pendingValues[i].value; + } + } + pendingValues.length = 0; +} + +public final function StoreSinglePistolValues() +{ + local int i; + i = 0; + while (i < storedValues.length) + { + if (storedValues[i].reference == none) + { + storedValues.Remove(i, 1); + continue; + } + storedValues[i].owner = storedValues[i].reference.instigator; + storedValues[i].value = storedValues[i].reference.sellValue; + i += 1; + } +} + +event Tick(float delta) +{ + 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' +} \ No newline at end of file diff --git a/sources/Features/FixDualiesCost/MutatorListener_FixDualiesCost.uc b/sources/Features/FixDualiesCost/MutatorListener_FixDualiesCost.uc new file mode 100644 index 0000000..0f209ae --- /dev/null +++ b/sources/Features/FixDualiesCost/MutatorListener_FixDualiesCost.uc @@ -0,0 +1,43 @@ +/** + * Overloaded mutator events listener to catch when pistol-type weapons + * (single or dual) are spawned and to correct their price. + * Copyright 2020 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 . + */ +class MutatorListener_FixDualiesCost extends MutatorListenerBase + abstract; + +static function bool CheckReplacement(Actor other, out byte isSuperRelevant) +{ + local KFWeapon weapon; + local FixDualiesCost dualiesCostFix; + weapon = KFWeapon(other); + if (weapon == none) return true; + dualiesCostFix = FixDualiesCost(class'FixDualiesCost'.static.GetInstance()); + if (dualiesCostFix == none) return true; + + dualiesCostFix.RegisterSinglePistol(weapon, true); + dualiesCostFix.FixCostAfterThrow(weapon); + dualiesCostFix.FixCostAfterBuying(weapon); + dualiesCostFix.FixCostAfterPickUp(weapon); + return true; +} + +defaultproperties +{ + relatedEvents = class'MutatorEvents' +} \ No newline at end of file