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