diff --git a/config/AcediaFixes.ini b/config/AcediaFixes.ini
index 964bd70..0751582 100644
--- a/config/AcediaFixes.ini
+++ b/config/AcediaFixes.ini
@@ -1,4 +1,4 @@
-[AcediaFixes.FixDualiesCost]
+[default FixDualiesCost]
; 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
@@ -43,7 +43,7 @@ autoEnable=true
allowSellValueIncrease=true
-[AcediaFixes.FixAmmoSelling]
+[default FixAmmoSelling]
; This feature addressed an oversight in vanilla code that
; allows clients to sell weapon's ammunition.
; Due to the implementation of ammo selling, this allows cheaters to
@@ -68,7 +68,7 @@ autoEnable=true
allowNegativeDosh=false
-[AcediaFixes.FixInventoryAbuse]
+[default FixInventoryAbuse]
; This feature addressed two issues with the inventory:
; 1. Players carrying amount of weapons that shouldn't be allowed by the
; weight limit.
@@ -94,7 +94,7 @@ dualiesClasses=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDu
dualiesClasses=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup')
-[AcediaFixes.FixInfiniteNades]
+[default FixInfiniteNades]
; This feature fixes a vulnerability in a code of `Frag` that can allow
; player to throw grenades even when he no longer has any.
; There's also no cooldowns on the throw, which can lead to a server crash.
@@ -106,7 +106,7 @@ autoEnable=true
ignoreTossFlags=true
-[AcediaFixes.FixDoshSpam]
+[default FixDoshSpam]
; 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
@@ -142,7 +142,7 @@ criticalDoshAmount=25
checkInterval=0.25
-[AcediaFixes.FixSpectatorCrash]
+[default FixSpectatorCrash]
; This feature attempts to prevent server crashes caused by someone
; quickly switching between being spectator and an active player.
autoEnable=true
@@ -170,7 +170,7 @@ spectatorChangeTimeout=0.25
allowServerBlock=true
-[AcediaFixes.FixFFHack]
+[default FixFFHack]
; This feature fixes a bug that can allow players to bypass server's
; friendly fire limitations and teamkill.
; Usual fixes apply friendly fire scale to suspicious damage themselves, which
@@ -225,7 +225,7 @@ alwaysScale=Class'KFMod.DamTypeZEDGunMKII'
; Damage types, for which we should never reaply friendly fire scaling.
;neverScale=Class'KFMod.???'
-[AcediaFixes.FixPipes]
+[default FixPipes]
; This feature addresses several bugs related to pipe bombs:
; 1. Gaining extra explosive damage by shooting pipes with
; a high fire rate weapon;
@@ -261,7 +261,7 @@ preventNPCDetonation=true
proximityCheckElevation=20
-[AcediaFixes.FixProjectileFF]
+[default FixProjectileFF]
; This feature addresses the bug that allows teammates to explode some of
; the player's projectiles by damaging them even when friendly fire is
; turned off, therefore killing the player (whether by accident or not).
@@ -272,7 +272,7 @@ autoEnable=true
; what, even when server has full friendly fire enabled.
ignoreFriendlyFire=false
-[AcediaFixes.FixZedTimeLags]
+[default FixZedTimeLags]
; When zed time activates, game speed is immediately set to
; `zedTimeSlomoScale` (0.2 by default), defined, like all other variables,
; in `KFGameType`. Zed time lasts `zedTimeDuration` seconds (3.0 by default),
@@ -319,7 +319,7 @@ disableTick=true
; changes will be to avoid solutions that are way too "hacky" and prefer some
; message spam getting through to the possibility of some unexpected gameplay
; effects as far as vanilla game is concerned.
-[AcediaFixes.FixLogSpam]
+[default FixLogSpam]
autoEnable=true
; This optionresponsible for fixing log spam
; due to picking up dropped weapons without set `inventory` variable.
diff --git a/sources/FixAmmoSelling/AmmoPickupStalker.uc b/sources/FixAmmoSelling/AmmoPickupStalker.uc
index 343bdab..d0dbc3a 100644
--- a/sources/FixAmmoSelling/AmmoPickupStalker.uc
+++ b/sources/FixAmmoSelling/AmmoPickupStalker.uc
@@ -51,13 +51,14 @@ public final static function StalkAmmoPickup(KFAmmoPickup newTarget)
event Touch(Actor other)
{
- local FixAmmoSelling ammoSellingFix;
+ local FixAmmoSelling_Feature ammoSellingFix;
if (target == none) return;
// If our box was sleeping for while (more than a tick), -
// player couldn't have gotten any ammo.
if (!wasActive && !target.IsInState('Pickup')) return;
- ammoSellingFix = FixAmmoSelling(class'FixAmmoSelling'.static.GetInstance());
+ ammoSellingFix = FixAmmoSelling_Feature(
+ class'FixAmmoSelling_Feature'.static.GetInstance());
if (ammoSellingFix != none) {
ammoSellingFix.RecordAmmoPickup(Pawn(other), target);
}
diff --git a/sources/FixAmmoSelling/FixAmmoSelling.uc b/sources/FixAmmoSelling/FixAmmoSelling.uc
index d9a942b..28e8b3b 100644
--- a/sources/FixAmmoSelling/FixAmmoSelling.uc
+++ b/sources/FixAmmoSelling/FixAmmoSelling.uc
@@ -1,20 +1,6 @@
/**
- * This feature addressed an oversight in vanilla code that
- * allows clients to sell weapon's ammunition.
- * Moreover, when being sold, ammunition cost is always multiplied by 0.75,
- * without taking into an account possible discount a player might have.
- * This allows cheaters to "print money" by buying and selling ammo over and
- * over again ammunition for some weapons,
- * notably pipe bombs (74% discount for lvl6 demolition)
- * and crossbow (42% discount for lvl6 sharpshooter).
- *
- * This feature fixes this problem by setting `pickupClass` variable in
- * potentially abusable weapons to our own value that won't receive a discount.
- * Luckily for us, it seems that pickup spawn and discount checks are the only
- * two place where variable is directly checked in a vanilla game's code
- * (`default.pickupClass` is used everywhere else),
- * so we can easily deal with the side effects of such change.
- * Copyright 2020 Anton Tarasenko
+ * Config object for `FixAmmoSelling_Feature`.
+ * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -31,347 +17,34 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class FixAmmoSelling extends Feature
+class FixAmmoSelling extends FeatureConfig
+ perobjectconfig
config(AcediaFixes);
-/**
- * We will replace `pickupClass` variable for all instances of potentially
- * abusable weapons. That is weapons, that have a discount for their ammunition
- * (via `GetAmmoCostScaling()` function in a corresponding perk class).
- * They are defined (along with our pickup replacements) in `rules` array.
- * That array isn't configurable, since the abusable status is hardcoded into
- * perk classes and the main mod that allows to change those (ServerPerks),
- * also solves ammo selling by a more direct method
- * (only available for the mods that replace player pawn class).
- * This change already completely fixes ammo printing.
- * Possible concern with changing the value of `pickupClass` is that
- * it might affect gameplay in too many ways.
- * But, luckily for us, that value is only used when spawning a new pickup and
- * in `ServerBuyAmmo` function of `KFPawn`
- * (all the other places use it's default value instead).
- * This means that the only two side-effects of our change are:
- * 1. That wrong pickup class will be spawned. This problem is easily
- * solved by replacing spawned actor in `CheckReplacement()`.
- * 2. That ammo will be sold at a different (lower for us) price,
- * while trader would still display and require the original price.
- * This problem is solved by manually taking from player the difference
- * between what he should have had to pay and what he actually paid.
- * This brings us to the second issue -
- * detecting when player bought the ammo.
- * Unfortunately, it doesn't seem possible to detect with 100% certainty
- * without replacing pawn or shop classes,
- * so we have to eliminate other possibilities.
- * There are seem to be three ways for players to get more ammo:
- * 1. For some mod to give it;
- * 2. Found it an ammo box;
- * 3. To buy ammo (can only happen in trader).
- * We don't want to provide mods with low-level API for bug fixes,
- * so to ensure the compatibility, mods that want to increase ammo values
- * will have to solve compatibility issue by themselves:
- * either by reimplementing this fix (possibly the best option)
- * or by giving players appropriate money along with the ammo.
- * The only other case we have to eliminate is ammo boxes.
- * First, all cases of ammo boxes outside the trader are easy to detect,
- * since in this case we can be sure that player didn't buy ammo
- * (and mods that can allow it can just get rid of
- * `ServerSellAmmo()` function directly, similarly to how ServerPerks does it).
- * We'll detect all the other boxes by attaching an auxiliary actor
- * (`AmmoPickupStalker`) to them, that will fire off `Touch()` event
- * at the same time as ammo boxes.
- * The only possible problem is that part of the ammo cost is
- * taken with a slight delay, which leaves cheaters a window of opportunity
- * to buy more than they can afford.
- * This issue is addressed by each ammo type costing as little as possible
- * (it's cost for corresponding perk at lvl6)
- * and a flag that does allow players to go into negative dosh values
- * (the cost is potential bugs in this fix itself, that
- * can somewhat affect regular players).
- */
-
-// Due to how this fix works, players with level below 6 get charged less
-// than necessary by the shop and this fix must take the rest of
-// the cost by itself.
-// The problem is, due to how ammo purchase is coded, low-level (<6 lvl)
-// players can actually buy more ammo for "fixed" weapons than they can afford
-// by filling ammo for one or all weapons.
-// Setting this flag to `true` will allow us to still take full cost
-// from them, putting them in "debt" (having negative dosh amount).
-// If you don't want to have players with negative dosh values on your server
-// as a side-effect of this fix, then leave this flag as `false`,
-// letting low level players buy ammo cheaper
-// (but not cheaper than lvl6 could).
-// NOTE: this issue doesn't affect level 6 players.
-// NOTE #2: this fix does give players below level 6 some
-// technical advantage compared to vanilla game, but this advantage
-// cannot exceed benefits of having level 6.
-var private config const bool allowNegativeDosh;
+var public config bool allowNegativeDosh;
-// This structure records what classes of weapons can be abused
-// and what pickup class we should use to fix the exploit.
-struct ReplacementRule
+protected function AssociativeArray ToData()
{
- var class abusableWeapon;
- var class pickupReplacement;
-};
-
-// Actual list of abusable weapons.
-var private const array rules;
-
-// We create one such record for any
-// abusable weapon instance in the game to store:
-struct WeaponRecord
-{
- // The instance itself.
- var KFWeapon weapon;
- // Corresponding ammo instance
- // (all abusable weapons only have one ammo type).
- var KFAmmunition ammo;
- // Last ammo amount we've seen, used to detect players gaining ammo
- // (from either ammo boxes or buying it).
- var int lastAmmoAmount;
-};
-
-protected function OnEnabled()
-{
- local LevelInfo level;
- local KFWeapon nextWeapon;
- local KFAmmoPickup nextPickup;
- level = _.unreal.GetLevel();
- // Find all abusable weapons
- foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon) {
- FixWeapon(nextWeapon);
- }
- // Start tracking all ammo boxes
- foreach level.DynamicActors(class'KFMod.KFAmmoPickup', nextPickup) {
- class'AmmoPickupStalker'.static.StalkAmmoPickup(nextPickup);
- }
+ local AssociativeArray data;
+ data = __().collections.EmptyAssociativeArray();
+ data.SetBool(P("allowNegativeDosh"), allowNegativeDosh, true);
+ return data;
}
-protected function OnDisabled()
+protected function FromData(AssociativeArray source)
{
- local int i;
- local LevelInfo level;
- local AmmoPickupStalker nextStalker;
- local array stalkers;
- local array registeredWeapons;
- level = _.unreal.GetLevel();
- registeredWeapons = FixAmmoSellingService(GetService()).registeredWeapons;
- // Restore all the `pickupClass` variables we've changed.
- for (i = 0; i < registeredWeapons.length; i += 1)
- {
- if (registeredWeapons[i].weapon != none)
- {
- registeredWeapons[i].weapon.pickupClass =
- registeredWeapons[i].weapon.default.pickupClass;
- }
+ if (source != none) {
+ allowNegativeDosh = source.GetBool(P("allowNegativeDosh"));
}
- // Kill all the stalkers;
- // to be safe, avoid destroying them directly in the iterator.
- foreach level.DynamicActors(class'AmmoPickupStalker', nextStalker) {
- stalkers[stalkers.length] = nextStalker;
- }
- for (i = 0; i < stalkers.length; i += 1)
- {
- if (stalkers[i] != none) {
- stalkers[i].Destroy();
- }
- }
-}
-
-// Checks if given class is a one of our pickup replacer classes.
-public static final function bool IsReplacer(class pickupClass)
-{
- local int i;
- if (pickupClass == none) return false;
- for (i = 0; i < default.rules.length; i += 1)
- {
- if (pickupClass == default.rules[i].pickupReplacement) {
- return true;
- }
- }
- return false;
}
-// 1. Checks if weapon can be abused and if it can, - fixes the problem.
-// 2. Starts tracking abusable weapon to detect when player buys ammo for it.
-public final function FixWeapon(KFWeapon potentialAbuser)
+protected function DefaultIt()
{
- local int i;
- local WeaponRecord newRecord;
- local array registeredWeapons;
- local FixAmmoSellingService service;
- if (potentialAbuser == none) return;
-
- service = FixAmmoSellingService(GetService());
- registeredWeapons = service.registeredWeapons;
- for (i = 0; i < registeredWeapons.length; i += 1)
- {
- if (registeredWeapons[i].weapon == potentialAbuser) {
- return;
- }
- }
- for (i = 0; i < rules.length; i += 1)
- {
- if (potentialAbuser.class == rules[i].abusableWeapon)
- {
- potentialAbuser.pickupClass = rules[i].pickupReplacement;
- newRecord.weapon = potentialAbuser;
- registeredWeapons[registeredWeapons.length] = newRecord;
- service.registeredWeapons = registeredWeapons;
- return;
- }
- }
-}
-
-// Finds ammo instance for recorded weapon in it's owner's inventory.
-public final function WeaponRecord FindAmmoInstance(WeaponRecord record)
-{
- local Inventory invIter;
- local KFAmmunition ammo;
- if (record.weapon == none) return record;
- if (record.weapon.instigator == none) return record;
-
- // Find instances anew
- invIter = record.weapon.instigator.inventory;
- while (invIter != none)
- {
- if (record.weapon.ammoClass[0] == invIter.class) {
- ammo = KFAmmunition(invIter);
- }
- invIter = invIter.inventory;
- }
- // Add missing instances
- if (ammo != none)
- {
- record.ammo = ammo;
- record.lastAmmoAmount = ammo.ammoAmount;
- }
- return record;
-}
-
-// Calculates how much more player should have paid for `ammoAmount`
-// amount of ammo, compared to how much trader took after our fix.
-private final function float GetPriceCorrection(
- KFWeapon kfWeapon,
- int ammoAmount
-)
-{
- local float boughtMagFraction;
- // `vanillaPrice` - price that would be calculated
- // without our interference
- // `fixPrice` - price that will be calculated after
- // we've replaced pickup class
- local float vanillaPrice, fixPrice;
- local KFPlayerReplicationInfo kfRI;
- local class vanillaPickupClass, fixPickupClass;
- if (kfWeapon == none || kfWeapon.instigator == none) return 0.0;
- fixPickupClass = class(kfWeapon.pickupClass);
- vanillaPickupClass = class(kfWeapon.default.pickupClass);
- if (fixPickupClass == none || vanillaPickupClass == none) return 0.0;
-
- // Calculate base prices
- boughtMagFraction = (float(ammoAmount) / kfWeapon.default.magCapacity);
- fixPrice = boughtMagFraction * fixPickupClass.default.AmmoCost;
- vanillaPrice = boughtMagFraction * vanillaPickupClass.default.AmmoCost;
- // Apply perk discount for vanilla price
- // (we don't need to consider secondary ammo or husk gun special cases,
- // since such weapons can't be abused via ammo dosh-printing)
- kfRI = KFPlayerReplicationInfo(kfWeapon.instigator.playerReplicationInfo);
- if (kfRI != none && kfRI.clientVeteranSkill != none)
- {
- vanillaPrice *= kfRI.clientVeteranSkill.static.
- GetAmmoCostScaling(kfRI, vanillaPickupClass);
- }
- // TWI's code rounds up ammo cost
- // to the integer value whenever ammo is bought,
- // so to calculate exactly how much we need to correct the cost,
- // we must find difference between the final, rounded cost values.
- return float(Max(0, int(vanillaPrice) - int(fixPrice)));
-}
-
-// Takes current ammo and last recorded in `record` value to calculate
-// how much money to take from the player
-// (calculations are done via `GetPriceCorrection()`).
-public final function WeaponRecord TaxAmmoChange(WeaponRecord record)
-{
- local int ammoDiff;
- local KFPawn taxPayer;
- local PlayerReplicationInfo replicationInfo;
- taxPayer = KFPawn(record.weapon.instigator);
- if (record.weapon == none || taxPayer == none) return record;
- // No need to charge money if player couldn't have
- // possibly bought the ammo.
- if (!taxPayer.CanBuyNow()) return record;
- // Find ammo difference with recorded value.
- if (record.ammo != none)
- {
- ammoDiff = Max(0, record.ammo.ammoAmount - record.lastAmmoAmount);
- record.lastAmmoAmount = record.ammo.ammoAmount;
- }
- // Make player pay dosh
- replicationInfo = taxPayer.playerReplicationInfo;
- if (replicationInfo != none)
- {
- replicationInfo.score -= GetPriceCorrection(record.weapon, ammoDiff);
- // This shouldn't happen, since shop is supposed to make sure
- // player has enough dosh to buy ammo at full price
- // (actual price + our correction).
- // But if user is extra concerned about it, -
- // we can additionally for force the score above 0.
- if (!allowNegativeDosh) {
- replicationInfo.score = FMax(0, replicationInfo.score);
- }
- }
- return record;
-}
-
-// Changes our records to account for player picking up the ammo box,
-// to avoid charging his for it.
-public final function RecordAmmoPickup(Pawn pawnWithAmmo, KFAmmoPickup pickup)
-{
- local int i;
- local int newAmount;
- local array registeredWeapons;
- local FixAmmoSellingService service;
- // Check conditions from `KFAmmoPickup` code (`Touch()` method)
- if (pickup == none) return;
- if (pawnWithAmmo == none) return;
- if (pawnWithAmmo.controller == none) return;
- if (!pawnWithAmmo.bCanPickupInventory) return;
- if (!pickup.FastTrace(pawnWithAmmo.location, pickup.location)) return;
-
- // Add relevant amount of ammo to our records
- service = FixAmmoSellingService(GetService());
- registeredWeapons = service.registeredWeapons;
- for (i = 0; i < registeredWeapons.length; i += 1)
- {
- if (registeredWeapons[i].weapon == none) continue;
- if (registeredWeapons[i].weapon.instigator == pawnWithAmmo)
- {
- newAmount = registeredWeapons[i].lastAmmoAmount
- + registeredWeapons[i].ammo.ammoPickupAmount;
- newAmount = Min(registeredWeapons[i].ammo.maxAmmo, newAmount);
- registeredWeapons[i].lastAmmoAmount = newAmount;
- }
- }
- service.registeredWeapons = registeredWeapons;
+ allowNegativeDosh = false;
}
defaultproperties
{
+ configName = "AcediaFixes"
allowNegativeDosh = false
- rules(0)=(abusableWeapon=class'KFMod.Crossbow',pickupReplacement=class'FixAmmoSellingClass_CrossbowPickup')
- rules(1)=(abusableWeapon=class'KFMod.PipeBombExplosive',pickupReplacement=class'FixAmmoSellingClass_PipeBombPickup')
- rules(2)=(abusableWeapon=class'KFMod.M79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M79Pickup')
- rules(3)=(abusableWeapon=class'KFMod.GoldenM79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_GoldenM79Pickup')
- rules(4)=(abusableWeapon=class'KFMod.M32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M32Pickup')
- rules(5)=(abusableWeapon=class'KFMod.CamoM32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_CamoM32Pickup')
- rules(6)=(abusableWeapon=class'KFMod.LAW',pickupReplacement=class'FixAmmoSellingClass_LAWPickup')
- rules(7)=(abusableWeapon=class'KFMod.SPGrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_SPGrenadePickup')
- rules(8)=(abusableWeapon=class'KFMod.SealSquealHarpoonBomber',pickupReplacement=class'FixAmmoSellingClass_SealSquealPickup')
- rules(9)=(abusableWeapon=class'KFMod.SeekerSixRocketLauncher',pickupReplacement=class'FixAmmoSellingClass_SeekerSixPickup')
- // Listeners
- requiredListeners(0) = class'MutatorListener_FixAmmoSelling'
- // Service
- serviceClass = class'FixAmmoSellingService'
}
\ No newline at end of file
diff --git a/sources/FixAmmoSelling/FixAmmoSellingService.uc b/sources/FixAmmoSelling/FixAmmoSellingService.uc
index b8c8857..c6d4ecc 100644
--- a/sources/FixAmmoSelling/FixAmmoSellingService.uc
+++ b/sources/FixAmmoSelling/FixAmmoSellingService.uc
@@ -18,14 +18,14 @@
* along with Acedia. If not, see .
*/
class FixAmmoSellingService extends FeatureService
- dependson(FixAmmoSelling);
+ dependson(FixAmmoSelling_Feature);
-var private FixAmmoSelling ammoSellingFix;
+var private FixAmmoSelling_Feature ammoSellingFix;
// All weapons we've detected so far.
// Made `public` to avoid needless calls, since this is not part of
// a library's interface anyway.
-var public array registeredWeapons;
+var public array registeredWeapons;
protected function Finalizer()
{
@@ -35,7 +35,7 @@ protected function Finalizer()
public function SetOwnerFeature(Feature newOwnerFeature)
{
super.SetOwnerFeature(newOwnerFeature);
- ammoSellingFix = FixAmmoSelling(newOwnerFeature);
+ ammoSellingFix = FixAmmoSelling_Feature(newOwnerFeature);
}
event Tick(float delta)
diff --git a/sources/FixAmmoSelling/FixAmmoSelling_Feature.uc b/sources/FixAmmoSelling/FixAmmoSelling_Feature.uc
new file mode 100644
index 0000000..d76807d
--- /dev/null
+++ b/sources/FixAmmoSelling/FixAmmoSelling_Feature.uc
@@ -0,0 +1,447 @@
+/**
+ * This feature addressed an oversight in vanilla code that
+ * allows clients to sell weapon's ammunition.
+ * Moreover, when being sold, ammunition cost is always multiplied by 0.75,
+ * without taking into an account possible discount a player might have.
+ * This allows cheaters to "print money" by buying and selling ammo over and
+ * over again ammunition for some weapons,
+ * notably pipe bombs (74% discount for lvl6 demolition)
+ * and crossbow (42% discount for lvl6 sharpshooter).
+ *
+ * This feature fixes this problem by setting `pickupClass` variable in
+ * potentially abusable weapons to our own value that won't receive a discount.
+ * Luckily for us, it seems that pickup spawn and discount checks are the only
+ * two place where variable is directly checked in a vanilla game's code
+ * (`default.pickupClass` is used everywhere else),
+ * so we can easily deal with the side effects of such change.
+ * 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 .
+ */
+class FixAmmoSelling_Feature extends Feature;
+
+/**
+ * We will replace `pickupClass` variable for all instances of potentially
+ * abusable weapons. That is weapons, that have a discount for their ammunition
+ * (via `GetAmmoCostScaling()` function in a corresponding perk class).
+ * They are defined (along with our pickup replacements) in `rules` array.
+ * That array isn't configurable, since the abusable status is hardcoded into
+ * perk classes and the main mod that allows to change those (ServerPerks),
+ * also solves ammo selling by a more direct method
+ * (only available for the mods that replace player pawn class).
+ * This change already completely fixes ammo printing.
+ * Possible concern with changing the value of `pickupClass` is that
+ * it might affect gameplay in too many ways.
+ * But, luckily for us, that value is only used when spawning a new pickup and
+ * in `ServerBuyAmmo` function of `KFPawn`
+ * (all the other places use it's default value instead).
+ * This means that the only two side-effects of our change are:
+ * 1. That wrong pickup class will be spawned. This problem is easily
+ * solved by replacing spawned actor in `CheckReplacement()`.
+ * 2. That ammo will be sold at a different (lower for us) price,
+ * while trader would still display and require the original price.
+ * This problem is solved by manually taking from player the difference
+ * between what he should have had to pay and what he actually paid.
+ * This brings us to the second issue -
+ * detecting when player bought the ammo.
+ * Unfortunately, it doesn't seem possible to detect with 100% certainty
+ * without replacing pawn or shop classes,
+ * so we have to eliminate other possibilities.
+ * There are seem to be three ways for players to get more ammo:
+ * 1. For some mod to give it;
+ * 2. Found it an ammo box;
+ * 3. To buy ammo (can only happen in trader).
+ * We don't want to provide mods with low-level API for bug fixes,
+ * so to ensure the compatibility, mods that want to increase ammo values
+ * will have to solve compatibility issue by themselves:
+ * either by reimplementing this fix (possibly the best option)
+ * or by giving players appropriate money along with the ammo.
+ * The only other case we have to eliminate is ammo boxes.
+ * First, all cases of ammo boxes outside the trader are easy to detect,
+ * since in this case we can be sure that player didn't buy ammo
+ * (and mods that can allow it can just get rid of
+ * `ServerSellAmmo()` function directly, similarly to how ServerPerks does it).
+ * We'll detect all the other boxes by attaching an auxiliary actor
+ * (`AmmoPickupStalker`) to them, that will fire off `Touch()` event
+ * at the same time as ammo boxes.
+ * The only possible problem is that part of the ammo cost is
+ * taken with a slight delay, which leaves cheaters a window of opportunity
+ * to buy more than they can afford.
+ * This issue is addressed by each ammo type costing as little as possible
+ * (it's cost for corresponding perk at lvl6)
+ * and a flag that does allow players to go into negative dosh values
+ * (the cost is potential bugs in this fix itself, that
+ * can somewhat affect regular players).
+ */
+
+// Due to how this fix works, players with level below 6 get charged less
+// than necessary by the shop and this fix must take the rest of
+// the cost by itself.
+// The problem is, due to how ammo purchase is coded, low-level (<6 lvl)
+// players can actually buy more ammo for "fixed" weapons than they can afford
+// by filling ammo for one or all weapons.
+// Setting this flag to `true` will allow us to still take full cost
+// from them, putting them in "debt" (having negative dosh amount).
+// If you don't want to have players with negative dosh values on your server
+// as a side-effect of this fix, then leave this flag as `false`,
+// letting low level players buy ammo cheaper
+// (but not cheaper than lvl6 could).
+// NOTE: this issue doesn't affect level 6 players.
+// NOTE #2: this fix does give players below level 6 some
+// technical advantage compared to vanilla game, but this advantage
+// cannot exceed benefits of having level 6.
+var private /*config*/ bool allowNegativeDosh;
+
+// This structure records what classes of weapons can be abused
+// and what pickup class we should use to fix the exploit.
+struct ReplacementRule
+{
+ var class abusableWeapon;
+ var class pickupReplacement;
+};
+
+// Actual list of abusable weapons.
+var private const array rules;
+
+// We create one such record for any
+// abusable weapon instance in the game to store:
+struct WeaponRecord
+{
+ // The instance itself.
+ var KFWeapon weapon;
+ // Corresponding ammo instance
+ // (all abusable weapons only have one ammo type).
+ var KFAmmunition ammo;
+ // Last ammo amount we've seen, used to detect players gaining ammo
+ // (from either ammo boxes or buying it).
+ var int lastAmmoAmount;
+};
+
+protected function OnEnabled()
+{
+ local LevelInfo level;
+ local KFWeapon nextWeapon;
+ local KFAmmoPickup nextPickup;
+ level = _.unreal.GetLevel();
+ _.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement;
+ // Find all abusable weapons
+ foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon) {
+ FixWeapon(nextWeapon);
+ }
+ // Start tracking all ammo boxes
+ foreach level.DynamicActors(class'KFMod.KFAmmoPickup', nextPickup) {
+ class'AmmoPickupStalker'.static.StalkAmmoPickup(nextPickup);
+ }
+}
+
+protected function OnDisabled()
+{
+ local int i;
+ local LevelInfo level;
+ local AmmoPickupStalker nextStalker;
+ local array stalkers;
+ local array registeredWeapons;
+ level = _.unreal.GetLevel();
+ _.unreal.mutator.OnCheckReplacement(self).Disconnect();
+ registeredWeapons = FixAmmoSellingService(GetService()).registeredWeapons;
+ // Restore all the `pickupClass` variables we've changed.
+ for (i = 0; i < registeredWeapons.length; i += 1)
+ {
+ if (registeredWeapons[i].weapon != none)
+ {
+ registeredWeapons[i].weapon.pickupClass =
+ registeredWeapons[i].weapon.default.pickupClass;
+ }
+ }
+ // Kill all the stalkers;
+ // to be safe, avoid destroying them directly in the iterator.
+ foreach level.DynamicActors(class'AmmoPickupStalker', nextStalker) {
+ stalkers[stalkers.length] = nextStalker;
+ }
+ for (i = 0; i < stalkers.length; i += 1)
+ {
+ if (stalkers[i] != none) {
+ stalkers[i].Destroy();
+ }
+ }
+}
+
+protected function SwapConfig(FeatureConfig config)
+{
+ local FixAmmoSelling newConfig;
+ newConfig = FixAmmoSelling(config);
+ if (newConfig == none) {
+ return;
+ }
+ allowNegativeDosh = newConfig.allowNegativeDosh;
+}
+
+// Checks if given class is a one of our pickup replacer classes.
+public static final function bool IsReplacer(class pickupClass)
+{
+ local int i;
+ if (pickupClass == none) return false;
+ for (i = 0; i < default.rules.length; i += 1)
+ {
+ if (pickupClass == default.rules[i].pickupReplacement) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// 1. Checks if weapon can be abused and if it can, - fixes the problem.
+// 2. Starts tracking abusable weapon to detect when player buys ammo for it.
+public final function FixWeapon(KFWeapon potentialAbuser)
+{
+ local int i;
+ local WeaponRecord newRecord;
+ local array registeredWeapons;
+ local FixAmmoSellingService service;
+ if (potentialAbuser == none) return;
+
+ service = FixAmmoSellingService(GetService());
+ registeredWeapons = service.registeredWeapons;
+ for (i = 0; i < registeredWeapons.length; i += 1)
+ {
+ if (registeredWeapons[i].weapon == potentialAbuser) {
+ return;
+ }
+ }
+ for (i = 0; i < rules.length; i += 1)
+ {
+ if (potentialAbuser.class == rules[i].abusableWeapon)
+ {
+ potentialAbuser.pickupClass = rules[i].pickupReplacement;
+ newRecord.weapon = potentialAbuser;
+ registeredWeapons[registeredWeapons.length] = newRecord;
+ service.registeredWeapons = registeredWeapons;
+ return;
+ }
+ }
+}
+
+// Finds ammo instance for recorded weapon in it's owner's inventory.
+public final function WeaponRecord FindAmmoInstance(WeaponRecord record)
+{
+ local Inventory invIter;
+ local KFAmmunition ammo;
+ if (record.weapon == none) return record;
+ if (record.weapon.instigator == none) return record;
+
+ // Find instances anew
+ invIter = record.weapon.instigator.inventory;
+ while (invIter != none)
+ {
+ if (record.weapon.ammoClass[0] == invIter.class) {
+ ammo = KFAmmunition(invIter);
+ }
+ invIter = invIter.inventory;
+ }
+ // Add missing instances
+ if (ammo != none)
+ {
+ record.ammo = ammo;
+ record.lastAmmoAmount = ammo.ammoAmount;
+ }
+ return record;
+}
+
+// Calculates how much more player should have paid for `ammoAmount`
+// amount of ammo, compared to how much trader took after our fix.
+private final function float GetPriceCorrection(
+ KFWeapon kfWeapon,
+ int ammoAmount
+)
+{
+ local float boughtMagFraction;
+ // `vanillaPrice` - price that would be calculated
+ // without our interference
+ // `fixPrice` - price that will be calculated after
+ // we've replaced pickup class
+ local float vanillaPrice, fixPrice;
+ local KFPlayerReplicationInfo kfRI;
+ local class vanillaPickupClass, fixPickupClass;
+ if (kfWeapon == none || kfWeapon.instigator == none) return 0.0;
+ fixPickupClass = class(kfWeapon.pickupClass);
+ vanillaPickupClass = class(kfWeapon.default.pickupClass);
+ if (fixPickupClass == none || vanillaPickupClass == none) return 0.0;
+
+ // Calculate base prices
+ boughtMagFraction = (float(ammoAmount) / kfWeapon.default.magCapacity);
+ fixPrice = boughtMagFraction * fixPickupClass.default.AmmoCost;
+ vanillaPrice = boughtMagFraction * vanillaPickupClass.default.AmmoCost;
+ // Apply perk discount for vanilla price
+ // (we don't need to consider secondary ammo or husk gun special cases,
+ // since such weapons can't be abused via ammo dosh-printing)
+ kfRI = KFPlayerReplicationInfo(kfWeapon.instigator.playerReplicationInfo);
+ if (kfRI != none && kfRI.clientVeteranSkill != none)
+ {
+ vanillaPrice *= kfRI.clientVeteranSkill.static.
+ GetAmmoCostScaling(kfRI, vanillaPickupClass);
+ }
+ // TWI's code rounds up ammo cost
+ // to the integer value whenever ammo is bought,
+ // so to calculate exactly how much we need to correct the cost,
+ // we must find difference between the final, rounded cost values.
+ return float(Max(0, int(vanillaPrice) - int(fixPrice)));
+}
+
+// Takes current ammo and last recorded in `record` value to calculate
+// how much money to take from the player
+// (calculations are done via `GetPriceCorrection()`).
+public final function WeaponRecord TaxAmmoChange(WeaponRecord record)
+{
+ local int ammoDiff;
+ local KFPawn taxPayer;
+ local PlayerReplicationInfo replicationInfo;
+ taxPayer = KFPawn(record.weapon.instigator);
+ if (record.weapon == none || taxPayer == none) return record;
+ // No need to charge money if player couldn't have
+ // possibly bought the ammo.
+ if (!taxPayer.CanBuyNow()) return record;
+ // Find ammo difference with recorded value.
+ if (record.ammo != none)
+ {
+ ammoDiff = Max(0, record.ammo.ammoAmount - record.lastAmmoAmount);
+ record.lastAmmoAmount = record.ammo.ammoAmount;
+ }
+ // Make player pay dosh
+ replicationInfo = taxPayer.playerReplicationInfo;
+ if (replicationInfo != none)
+ {
+ replicationInfo.score -= GetPriceCorrection(record.weapon, ammoDiff);
+ // This shouldn't happen, since shop is supposed to make sure
+ // player has enough dosh to buy ammo at full price
+ // (actual price + our correction).
+ // But if user is extra concerned about it, -
+ // we can additionally for force the score above 0.
+ if (!allowNegativeDosh) {
+ replicationInfo.score = FMax(0, replicationInfo.score);
+ }
+ }
+ return record;
+}
+
+// Changes our records to account for player picking up the ammo box,
+// to avoid charging his for it.
+public final function RecordAmmoPickup(Pawn pawnWithAmmo, KFAmmoPickup pickup)
+{
+ local int i;
+ local int newAmount;
+ local array registeredWeapons;
+ local FixAmmoSellingService service;
+ // Check conditions from `KFAmmoPickup` code (`Touch()` method)
+ if (pickup == none) return;
+ if (pawnWithAmmo == none) return;
+ if (pawnWithAmmo.controller == none) return;
+ if (!pawnWithAmmo.bCanPickupInventory) return;
+ if (!pickup.FastTrace(pawnWithAmmo.location, pickup.location)) return;
+
+ // Add relevant amount of ammo to our records
+ service = FixAmmoSellingService(GetService());
+ registeredWeapons = service.registeredWeapons;
+ for (i = 0; i < registeredWeapons.length; i += 1)
+ {
+ if (registeredWeapons[i].weapon == none) continue;
+ if (registeredWeapons[i].weapon.instigator == pawnWithAmmo)
+ {
+ newAmount = registeredWeapons[i].lastAmmoAmount
+ + registeredWeapons[i].ammo.ammoPickupAmount;
+ newAmount = Min(registeredWeapons[i].ammo.maxAmmo, newAmount);
+ registeredWeapons[i].lastAmmoAmount = newAmount;
+ }
+ }
+ service.registeredWeapons = registeredWeapons;
+}
+
+private final function bool CheckReplacement(
+ Actor other,
+ out byte isSuperRelevant)
+{
+ if (other == none) {
+ return true;
+ }
+ // We need to replace pickup classes back,
+ // as they might not even exist on clients.
+ if (IsReplacer(other.class))
+ {
+ ReplaceOldPickup(Pickup(other));
+ return false;
+ }
+ FixWeapon(KFWeapon(other));
+ // If it's ammo pickup - we need to stalk it
+ class'AmmoPickupStalker'.static.StalkAmmoPickup(KFAmmoPickup(other));
+ return true;
+}
+
+// This function recreates the logic of `KFWeapon.DropFrom()`,
+// since standard `ReplaceWith()` method produces bad results.
+private function ReplaceOldPickup(Pickup oldPickup)
+{
+ local Pawn instigator;
+ local Pickup newPickup;
+ local KFWeapon relevantWeapon;
+ if (oldPickup == none) return;
+ instigator = oldPickup.instigator;
+ if (instigator == none) return;
+ relevantWeapon = GetWeaponOfClass(instigator, oldPickup.inventoryType);
+ if (relevantWeapon == none) return;
+
+ newPickup = relevantWeapon.Spawn( relevantWeapon.default.pickupClass,,,
+ relevantWeapon.location);
+ newPickup.InitDroppedPickupFor(relevantWeapon);
+ newPickup.velocity = relevantWeapon.velocity +
+ Vector(instigator.rotation) * 100;
+ if (instigator.health > 0)
+ KFWeaponPickup(newPickup).bThrown = true;
+}
+
+static 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;
+}
+
+defaultproperties
+{
+ configClass = class'FixAmmoSelling'
+ allowNegativeDosh = false
+ rules(0)=(abusableWeapon=class'KFMod.Crossbow',pickupReplacement=class'FixAmmoSellingClass_CrossbowPickup')
+ rules(1)=(abusableWeapon=class'KFMod.PipeBombExplosive',pickupReplacement=class'FixAmmoSellingClass_PipeBombPickup')
+ rules(2)=(abusableWeapon=class'KFMod.M79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M79Pickup')
+ rules(3)=(abusableWeapon=class'KFMod.GoldenM79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_GoldenM79Pickup')
+ rules(4)=(abusableWeapon=class'KFMod.M32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M32Pickup')
+ rules(5)=(abusableWeapon=class'KFMod.CamoM32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_CamoM32Pickup')
+ rules(6)=(abusableWeapon=class'KFMod.LAW',pickupReplacement=class'FixAmmoSellingClass_LAWPickup')
+ rules(7)=(abusableWeapon=class'KFMod.SPGrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_SPGrenadePickup')
+ rules(8)=(abusableWeapon=class'KFMod.SealSquealHarpoonBomber',pickupReplacement=class'FixAmmoSellingClass_SealSquealPickup')
+ rules(9)=(abusableWeapon=class'KFMod.SeekerSixRocketLauncher',pickupReplacement=class'FixAmmoSellingClass_SeekerSixPickup')
+ // Service
+ serviceClass = class'FixAmmoSellingService'
+}
\ No newline at end of file
diff --git a/sources/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc b/sources/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc
deleted file mode 100644
index 0c7a528..0000000
--- a/sources/FixAmmoSelling/MutatorListener_FixAmmoSelling.uc
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * Overloaded mutator events listener to register every new
- * spawned weapon and ammo pickup.
- * 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_FixAmmoSelling extends MutatorListenerBase
- abstract;
-
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- if (other == none) return true;
-
- // We need to replace pickup classes back,
- // as they might not even exist on clients.
- if (class'FixAmmoSelling'.static.IsReplacer(other.class))
- {
- ReplaceOldPickup(Pickup(other));
- return false;
- }
- CheckAbusableWeapon(KFWeapon(other));
- // If it's ammo pickup - we need to stalk it
- class'AmmoPickupStalker'.static.StalkAmmoPickup(KFAmmoPickup(other));
- return true;
-}
-
-private static function CheckAbusableWeapon(KFWeapon newWeapon)
-{
- local FixAmmoSelling ammoSellingFix;
- if (newWeapon == none) return;
- ammoSellingFix = FixAmmoSelling(class'FixAmmoSelling'.static.GetInstance());
- if (ammoSellingFix == none) return;
- ammoSellingFix.FixWeapon(newWeapon);
-}
-
-// This function recreates the logic of `KFWeapon.DropFrom()`,
-// since standard `ReplaceWith()` method produces bad results.
-private static function ReplaceOldPickup(Pickup oldPickup)
-{
- local Pawn instigator;
- local Pickup newPickup;
- local KFWeapon relevantWeapon;
- if (oldPickup == none) return;
- instigator = oldPickup.instigator;
- if (instigator == none) return;
- relevantWeapon = GetWeaponOfClass(instigator, oldPickup.inventoryType);
- if (relevantWeapon == none) return;
-
- newPickup = relevantWeapon.Spawn( relevantWeapon.default.pickupClass,,,
- relevantWeapon.location);
- newPickup.InitDroppedPickupFor(relevantWeapon);
- newPickup.velocity = relevantWeapon.velocity +
- Vector(instigator.rotation) * 100;
- if (instigator.health > 0)
- KFWeaponPickup(newPickup).bThrown = true;
-}
-
-static 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;
-}
-
-defaultproperties
-{
- relatedEvents = class'MutatorEvents'
-}
\ No newline at end of file
diff --git a/sources/FixDoshSpam/FixDoshSpam.uc b/sources/FixDoshSpam/FixDoshSpam.uc
index 73b9a84..9a55cc8 100644
--- a/sources/FixDoshSpam/FixDoshSpam.uc
+++ b/sources/FixDoshSpam/FixDoshSpam.uc
@@ -1,14 +1,6 @@
/**
- * 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
+ * Config object for `FixDoshSpam_Feature`.
+ * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -25,235 +17,50 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class FixDoshSpam extends Feature
+class FixDoshSpam extends FeatureConfig
+ perobjectconfig
config(AcediaFixes);
-/**
- * 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 const float doshPerSecondLimitMax;
-var private config const 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 const 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 const 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 currentContributors;
-
-// Wads of cash that are lying around on the map.
-var private array wads;
-
-// Generates "reset" events when `wads` array is getting cleaned from
-// destroyed/picked up dosh and players' contributions are reduced.
-var private RealTimer checkTimer;
+var public /*config*/ float doshPerSecondLimitMax;
+var public /*config*/ float doshPerSecondLimitMin;
+var public /*config*/ int criticalDoshAmount;
+var public /*config*/ float checkInterval;
-protected function OnEnabled()
+protected function AssociativeArray ToData()
{
- local LevelInfo level;
- local CashPickup nextCash;
- checkTimer = _.time.StartRealTimer(checkInterval, true);
- checkTimer.OnElapsed(self).connect = Tick;
- level = _.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] = _.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;
- checkTimer.FreeSelf();
-}
-
-// 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] = _.unreal.ActorRef(cash);
- // Add contribution to player
- playerIndex = GetContributorIndex(player);
- if (playerIndex >= 0)
- {
- currentContributors[playerIndex].contribution += 1.0;
- return;
- }
- newStreamRecord.player = _.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;
- }
- }
+ local AssociativeArray data;
+ data = __().collections.EmptyAssociativeArray();
+ data.SetFloat(P("doshPerSecondLimitMax"), doshPerSecondLimitMax, true);
+ data.SetFloat(P("doshPerSecondLimitMin"), doshPerSecondLimitMin, true);
+ data.SetInt(P("criticalDoshAmount"), criticalDoshAmount, true);
+ data.SetFloat(P("checkInterval"), checkInterval, true);
+ return data;
}
-// Don't track players that no longer contribute to dosh generation.
-private final function RemoveNonContributors()
+protected function FromData(AssociativeArray source)
{
- local int i;
- local array updContributors;
- for (i = 0; i < currentContributors.length; i += 1)
+ if (source != none)
{
- // 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];
- }
+ doshPerSecondLimitMax = source.GetFloat(P("doshPerSecondLimitMax"), 50);
+ doshPerSecondLimitMin = source.GetFloat(P("doshPerSecondLimitMin"), 5);
+ criticalDoshAmount = source.GetInt(P("criticalDoshAmount"), 25);
+ checkInterval = source.GetFloat(P("checkInterval"), 0.25);
}
- currentContributors = updContributors;
}
-private function Tick(Timer source)
+protected function DefaultIt()
{
- CleanWadsArray();
- ReducePlayerContributions();
- RemoveNonContributors();
+ doshPerSecondLimitMax = 50;
+ doshPerSecondLimitMin = 5;
+ criticalDoshAmount = 25;
+ checkInterval = 0.25;
}
defaultproperties
{
+ configName = "AcediaFixes"
doshPerSecondLimitMax = 50
doshPerSecondLimitMin = 5
criticalDoshAmount = 25
checkInterval = 0.25
- // Listeners
- requiredListeners(0) = class'MutatorListener_FixDoshSpam'
}
\ No newline at end of file
diff --git a/sources/FixDoshSpam/FixDoshSpam_Feature.uc b/sources/FixDoshSpam/FixDoshSpam_Feature.uc
new file mode 100644
index 0000000..fc12266
--- /dev/null
+++ b/sources/FixDoshSpam/FixDoshSpam_Feature.uc
@@ -0,0 +1,298 @@
+/**
+ * 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 .
+ */
+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 currentContributors;
+
+// Wads of cash that are lying around on the map.
+var private array wads;
+
+// Generates "reset" events when `wads` array is getting cleaned from
+// destroyed/picked up dosh and players' contributions are reduced.
+var private RealTimer checkTimer;
+
+protected function OnEnabled()
+{
+ local LevelInfo level;
+ local CashPickup nextCash;
+ checkTimer = _.time.StartRealTimer(checkInterval, true);
+ checkTimer.OnElapsed(self).connect = Tick;
+ _.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement;
+ level = _.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] = _.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;
+ _.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] = _.unreal.ActorRef(cash);
+ // Add contribution to player
+ playerIndex = GetContributorIndex(player);
+ if (playerIndex >= 0)
+ {
+ currentContributors[playerIndex].contribution += 1.0;
+ return;
+ }
+ newStreamRecord.player = _.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 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
+}
\ No newline at end of file
diff --git a/sources/FixDoshSpam/MutatorListener_FixDoshSpam.uc b/sources/FixDoshSpam/MutatorListener_FixDoshSpam.uc
deleted file mode 100644
index b173f0a..0000000
--- a/sources/FixDoshSpam/MutatorListener_FixDoshSpam.uc
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Overloaded mutator events listener to catch and, possibly,
- * prevent spawning dosh actors.
- * 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 .
- */
-class MutatorListener_FixDoshSpam extends MutatorListenerBase
- abstract;
-
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- local FixDoshSpam doshFix;
- 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;
- doshFix = FixDoshSpam(class'FixDoshSpam'.static.GetInstance());
- if (doshFix == 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 (doshFix.IsDoshStreamOverLimit() && doshFix.IsContributor(player)) {
- return false;
- }
- // If we do spawn cash - record this contribution.
- doshFix.AddContribution(player, CashPickup(other));
- return true;
-}
-
-defaultproperties
-{
- relatedEvents = class'MutatorEvents'
-}
\ No newline at end of file
diff --git a/sources/FixDualiesCost/FixDualiesCost.uc b/sources/FixDualiesCost/FixDualiesCost.uc
index 93da39c..1c913cd 100644
--- a/sources/FixDualiesCost/FixDualiesCost.uc
+++ b/sources/FixDualiesCost/FixDualiesCost.uc
@@ -1,15 +1,6 @@
/**
- * 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
+ * Config object for `FixDualiesCost_Feature`.
+ * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -26,434 +17,36 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class FixDualiesCost extends Feature
+class FixDualiesCost extends FeatureConfig
+ perobjectconfig
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 single;
- var class dual;
-};
-var private const array 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 pendingValues;
-
-// Describe sell values of all currently existing single pistols.
-struct WeaponDataRecord
-{
- // Reference to `KFWeapon` instance
- var NativeActorRef 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.
- // Reference to `Pawn`.
- var NativeActorRef owner;
-};
-var private const array 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 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;
-}
+var public config bool allowSellValueIncrease;
-// Calculates full cost of a weapon with a discount,
-// dependent on it's instigator's perk.
-private final function float GetFullCost(KFWeapon weapon)
+protected function AssociativeArray ToData()
{
- 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 = _.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;
+ local AssociativeArray data;
+ data = __().collections.EmptyAssociativeArray();
+ data.SetBool(P("allowSellValueIncrease"), allowSellValueIncrease, true);
+ return data;
}
-private final function StoreSinglePistolValues()
+protected function FromData(AssociativeArray source)
{
- local int i;
- local KFWeapon nextWeapon;
- while (i < storedValues.length)
+ if (source != none)
{
- 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;
+ allowSellValueIncrease =
+ source.GetBool(P("allowSellValueIncrease"), true);
}
}
-private function Tick(float delta, float timeDilationCoefficient)
+protected function DefaultIt()
{
- ApplyPendingValues();
- StoreSinglePistolValues();
+ allowSellValueIncrease = true;
}
defaultproperties
{
+ configName = "AcediaFixes"
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/FixDualiesCost/FixDualiesCost_Feature.uc b/sources/FixDualiesCost/FixDualiesCost_Feature.uc
new file mode 100644
index 0000000..8dc0cb1
--- /dev/null
+++ b/sources/FixDualiesCost/FixDualiesCost_Feature.uc
@@ -0,0 +1,485 @@
+/**
+ * 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 .
+ */
+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 single;
+ var class dual;
+};
+var private const array 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 pendingValues;
+
+// Describe sell values of all currently existing single pistols.
+struct WeaponDataRecord
+{
+ // Reference to `KFWeapon` instance
+ var NativeActorRef 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.
+ // Reference to `Pawn`.
+ var NativeActorRef owner;
+};
+var private const array 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 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 = _.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')
+}
\ No newline at end of file
diff --git a/sources/FixDualiesCost/MutatorListener_FixDualiesCost.uc b/sources/FixDualiesCost/MutatorListener_FixDualiesCost.uc
deleted file mode 100644
index 0f209ae..0000000
--- a/sources/FixDualiesCost/MutatorListener_FixDualiesCost.uc
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * 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
diff --git a/sources/FixFFHack/FixFFHack.uc b/sources/FixFFHack/FixFFHack.uc
index 37d3f2c..97964b2 100644
--- a/sources/FixFFHack/FixFFHack.uc
+++ b/sources/FixFFHack/FixFFHack.uc
@@ -1,12 +1,6 @@
/**
- * This feature fixes a bug that can allow players to bypass server's
- * friendly fire limitations and teamkill.
- * Usual fixes apply friendly fire scale to suspicious damage themselves, which
- * also disables some of the environmental damage.
- * In order to avoid that, this fix allows server owner to define precisely
- * to what damage types to apply the friendly fire scaling.
- * It should be all damage types related to projectiles.
- * Copyright 2019 - 2021 Anton Tarasenko
+ * Config object for `FixFFHack_Feature`.
+ * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -23,117 +17,108 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class FixFFHack extends Feature
+class FixFFHack extends FeatureConfig
+ perobjectconfig
config(AcediaFixes);
-/**
- * It's possible to bypass friendly fire damage scaling and always deal
- * full damage to other players, if one were to either leave the server or
- * spectate right after shooting a projectile. We use game rules to catch
- * such occurrences and apply friendly fire scaling to weapons,
- * specified by server admins.
- * To specify required subset of weapons, one must first
- * chose a general rule (scale by default / don't scale by default) and then,
- * optionally, add exceptions to it.
- * Choosing `scaleByDefault == true` as a general rule will make this fix
- * behave in the similar way to `KFExplosiveFix` by mutant and will disable
- * some environmental sources of damage on some maps. One can then add relevant
- * damage classes as exceptions to fix that downside, but making an extensive
- * list of such sources might prove problematic.
- * On the other hand, setting `scaleByDefault == false` will allow to get
- * rid of team-killing exploits by simply adding damage types of all
- * projectile weapons, used on a server. This fix comes with such filled-in
- * list of all vanilla projectile classes.
- */
-
-// Defines a general rule for choosing whether or not to apply
-// friendly fire scaling.
-// This can be overwritten by exceptions (`alwaysScale` or `neverScale`).
-// Enabling scaling by default without any exceptions in `neverScale` will
-// make this fix behave almost identically to Mutant's
-// 'Explosives Fix Mutator'.
-var private config const bool scaleByDefault;
-// Damage types, for which we should always reapply friendly fire scaling.
-var private config const array< class > alwaysScale;
-// Damage types, for which we should never reapply friendly fire scaling.
-var private config const array< class > neverScale;
-
-protected function OnEnabled()
-{
- _.unreal.gameRules.OnNetDamage(self).connect = NetDamage;
-}
+var public config bool scaleByDefault;
+var public config array< class > alwaysScale;
+var public config array< class > neverScale;
-protected function OnDisabled()
+protected function AssociativeArray ToData()
{
- _.unreal.gameRules.OnNetDamage(self).Disconnect();
+ local int i;
+ local DynamicArray damageTypeArray;
+ local AssociativeArray data;
+ data = _.collections.EmptyAssociativeArray();
+ data.SetBool(P("scaleByDefault"), scaleByDefault, true);
+ damageTypeArray = _.collections.EmptyDynamicArray();
+ for (i = 0; i < alwaysScale.length; i += 1) {
+ damageTypeArray.AddItem(_.text.FromString(string(alwaysScale[i])));
+ }
+ data.SetItem(P("alwaysScale"), damageTypeArray);
+ damageTypeArray = _.collections.EmptyDynamicArray();
+ for (i = 0; i < neverScale.length; i += 1) {
+ damageTypeArray.AddItem(_.text.FromString(string(neverScale[i])));
+ }
+ data.SetItem(P("neverScale"), damageTypeArray);
+ return data;
}
-function int NetDamage(
- int originalDamage,
- int damage,
- Pawn injured,
- Pawn instigator,
- Vector hitLocation,
- out Vector momentum,
- class damageType)
+protected function FromData(AssociativeArray source)
{
- // Something is very wrong and we can just bail on this damage
- if (damageType == none) {
- return 0;
+ local int i;
+ local DynamicArray damageTypeArray;
+ if (source == none) {
+ return;
}
- // We only check when suspicious instigators that aren't a world
- if (!damageType.default.bCausedByWorld && IsSuspicious(instigator))
- {
- if (ShouldScaleDamage(damageType))
+ scaleByDefault = source.GetBool(P("scaleByDefault"));
+ alwaysScale.length = 0;
+ damageTypeArray = source.GetDynamicArray(P("alwaysScale"));
+ if (damageTypeArray != none) {
+ for (i = 0; i < damageTypeArray.GetLength(); i += 1)
{
- // Remove pushback to avoid environmental kills
- momentum = Vect(0.0, 0.0, 0.0);
- damage *= _.unreal.GetKFGameType().friendlyFireScale;
+ alwaysScale[i] = class(
+ _.memory.LoadClass(damageTypeArray.GetText(i)));
}
}
- return damage;
-}
-
-private function bool IsSuspicious(Pawn instigator)
-{
- // Instigator vanished
- if (instigator == none) return true;
-
- // Instigator already became spectator
- if (KFPawn(instigator) != none)
- {
- if (instigator.playerReplicationInfo != none) {
- return instigator.playerReplicationInfo.bOnlySpectator;
+ neverScale.length = 0;
+ damageTypeArray = source.GetDynamicArray(P("neverScale"));
+ if (damageTypeArray != none) {
+ for (i = 0; i < damageTypeArray.GetLength(); i += 1)
+ {
+ neverScale[i] = class(
+ _.memory.LoadClass(damageTypeArray.GetText(i)));
}
- return true; // Replication info is gone => suspicious
}
- return false;
}
-// Checks general rule and exception list
-public final function bool ShouldScaleDamage(class damageType)
+protected function DefaultIt()
{
- local int i;
- local array< class > exceptions;
- if (damageType == none) return false;
-
- if (scaleByDefault) {
- exceptions = neverScale;
- }
- else {
- exceptions = alwaysScale;
- }
- for (i = 0; i < exceptions.length; i += 1)
- {
- if (exceptions[i] == damageType) {
- return (!scaleByDefault);
- }
- }
- return scaleByDefault;
+ scaleByDefault = false;
+ alwaysScale.length = 0;
+ neverScale.length = 0;
+ // Vanilla damage types for projectiles
+ alwaysScale[0] = class'KFMod.DamTypeCrossbuzzsawHeadShot';
+ alwaysScale[1] = class'KFMod.DamTypeCrossbuzzsaw';
+ alwaysScale[2] = class'KFMod.DamTypeFrag';
+ alwaysScale[3] = class'KFMod.DamTypePipeBomb';
+ alwaysScale[4] = class'KFMod.DamTypeM203Grenade';
+ alwaysScale[5] = class'KFMod.DamTypeM79Grenade';
+ alwaysScale[6] = class'KFMod.DamTypeM79GrenadeImpact';
+ alwaysScale[7] = class'KFMod.DamTypeM32Grenade';
+ alwaysScale[8] = class'KFMod.DamTypeLAW';
+ alwaysScale[9] = class'KFMod.DamTypeLawRocketImpact';
+ alwaysScale[10] = class'KFMod.DamTypeFlameNade';
+ alwaysScale[11] = class'KFMod.DamTypeFlareRevolver';
+ alwaysScale[12] = class'KFMod.DamTypeFlareProjectileImpact';
+ alwaysScale[13] = class'KFMod.DamTypeBurned';
+ alwaysScale[14] = class'KFMod.DamTypeTrenchgun';
+ alwaysScale[15] = class'KFMod.DamTypeHuskGun';
+ alwaysScale[16] = class'KFMod.DamTypeCrossbow';
+ alwaysScale[17] = class'KFMod.DamTypeCrossbowHeadShot';
+ alwaysScale[18] = class'KFMod.DamTypeM99SniperRifle';
+ alwaysScale[19] = class'KFMod.DamTypeM99HeadShot';
+ alwaysScale[20] = class'KFMod.DamTypeShotgun';
+ alwaysScale[21] = class'KFMod.DamTypeNailGun';
+ alwaysScale[22] = class'KFMod.DamTypeDBShotgun';
+ alwaysScale[23] = class'KFMod.DamTypeKSGShotgun';
+ alwaysScale[24] = class'KFMod.DamTypeBenelli';
+ alwaysScale[25] = class'KFMod.DamTypeSPGrenade';
+ alwaysScale[26] = class'KFMod.DamTypeSPGrenadeImpact';
+ alwaysScale[27] = class'KFMod.DamTypeSeekerSixRocket';
+ alwaysScale[28] = class'KFMod.DamTypeSeekerRocketImpact';
+ alwaysScale[29] = class'KFMod.DamTypeSealSquealExplosion';
+ alwaysScale[30] = class'KFMod.DamTypeRocketImpact';
+ alwaysScale[31] = class'KFMod.DamTypeBlowerThrower';
+ alwaysScale[32] = class'KFMod.DamTypeSPShotgun';
+ alwaysScale[33] = class'KFMod.DamTypeZEDGun';
+ alwaysScale[34] = class'KFMod.DamTypeZEDGunMKII';
}
defaultproperties
{
+ configName = "AcediaFixes"
scaleByDefault = false
// Vanilla damage types for projectiles
alwaysScale(0) = class'KFMod.DamTypeCrossbuzzsawHeadShot'
diff --git a/sources/FixFFHack/FixFFHack_Feature.uc b/sources/FixFFHack/FixFFHack_Feature.uc
new file mode 100644
index 0000000..608acea
--- /dev/null
+++ b/sources/FixFFHack/FixFFHack_Feature.uc
@@ -0,0 +1,186 @@
+/**
+ * This feature fixes a bug that can allow players to bypass server's
+ * friendly fire limitations and teamkill.
+ * Usual fixes apply friendly fire scale to suspicious damage themselves, which
+ * also disables some of the environmental damage.
+ * In order to avoid that, this fix allows server owner to define precisely
+ * to what damage types to apply the friendly fire scaling.
+ * It should be all damage types related to projectiles.
+ * 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 .
+ */
+class FixFFHack_Feature extends Feature;
+
+/**
+ * It's possible to bypass friendly fire damage scaling and always deal
+ * full damage to other players, if one were to either leave the server or
+ * spectate right after shooting a projectile. We use game rules to catch
+ * such occurrences and apply friendly fire scaling to weapons,
+ * specified by server admins.
+ * To specify required subset of weapons, one must first
+ * chose a general rule (scale by default / don't scale by default) and then,
+ * optionally, add exceptions to it.
+ * Choosing `scaleByDefault == true` as a general rule will make this fix
+ * behave in the similar way to `KFExplosiveFix` by mutant and will disable
+ * some environmental sources of damage on some maps. One can then add relevant
+ * damage classes as exceptions to fix that downside, but making an extensive
+ * list of such sources might prove problematic.
+ * On the other hand, setting `scaleByDefault == false` will allow to get
+ * rid of team-killing exploits by simply adding damage types of all
+ * projectile weapons, used on a server. This fix comes with such filled-in
+ * list of all vanilla projectile classes.
+ */
+
+// Defines a general rule for choosing whether or not to apply
+// friendly fire scaling.
+// This can be overwritten by exceptions (`alwaysScale` or `neverScale`).
+// Enabling scaling by default without any exceptions in `neverScale` will
+// make this fix behave almost identically to Mutant's
+// 'Explosives Fix Mutator'.
+var private /*config*/ bool scaleByDefault;
+// Damage types, for which we should always reapply friendly fire scaling.
+var private /*config*/ array< class > alwaysScale;
+// Damage types, for which we should never reapply friendly fire scaling.
+var private /*config*/ array< class > neverScale;
+
+protected function OnEnabled()
+{
+ _.unreal.gameRules.OnNetDamage(self).connect = NetDamage;
+}
+
+protected function OnDisabled()
+{
+ _.unreal.gameRules.OnNetDamage(self).Disconnect();
+}
+
+protected function SwapConfig(FeatureConfig config)
+{
+ local FixFFHack newConfig;
+ newConfig = FixFFHack(config);
+ if (newConfig == none) {
+ return;
+ }
+ scaleByDefault = newConfig.scaleByDefault;
+ alwaysScale = newConfig.alwaysScale;
+ neverScale = newConfig.neverScale;
+}
+
+function int NetDamage(
+ int originalDamage,
+ int damage,
+ Pawn injured,
+ Pawn instigator,
+ Vector hitLocation,
+ out Vector momentum,
+ class damageType)
+{
+ // Something is very wrong and we can just bail on this damage
+ if (damageType == none) {
+ return 0;
+ }
+ // We only check when suspicious instigators that aren't a world
+ if (!damageType.default.bCausedByWorld && IsSuspicious(instigator))
+ {
+ if (ShouldScaleDamage(damageType))
+ {
+ // Remove pushback to avoid environmental kills
+ momentum = Vect(0.0, 0.0, 0.0);
+ damage *= _.unreal.GetKFGameType().friendlyFireScale;
+ }
+ }
+ return damage;
+}
+
+private function bool IsSuspicious(Pawn instigator)
+{
+ // Instigator vanished
+ if (instigator == none) return true;
+
+ // Instigator already became spectator
+ if (KFPawn(instigator) != none)
+ {
+ if (instigator.playerReplicationInfo != none) {
+ return instigator.playerReplicationInfo.bOnlySpectator;
+ }
+ return true; // Replication info is gone => suspicious
+ }
+ return false;
+}
+
+// Checks general rule and exception list
+public final function bool ShouldScaleDamage(class damageType)
+{
+ local int i;
+ local array< class > exceptions;
+ if (damageType == none) return false;
+
+ if (scaleByDefault) {
+ exceptions = neverScale;
+ }
+ else {
+ exceptions = alwaysScale;
+ }
+ for (i = 0; i < exceptions.length; i += 1)
+ {
+ if (exceptions[i] == damageType) {
+ return (!scaleByDefault);
+ }
+ }
+ return scaleByDefault;
+}
+
+defaultproperties
+{
+ configClass = class'FixFFHack'
+ scaleByDefault = false
+ // Vanilla damage types for projectiles
+ alwaysScale(0) = class'KFMod.DamTypeCrossbuzzsawHeadShot'
+ alwaysScale(1) = class'KFMod.DamTypeCrossbuzzsaw'
+ alwaysScale(2) = class'KFMod.DamTypeFrag'
+ alwaysScale(3) = class'KFMod.DamTypePipeBomb'
+ alwaysScale(4) = class'KFMod.DamTypeM203Grenade'
+ alwaysScale(5) = class'KFMod.DamTypeM79Grenade'
+ alwaysScale(6) = class'KFMod.DamTypeM79GrenadeImpact'
+ alwaysScale(7) = class'KFMod.DamTypeM32Grenade'
+ alwaysScale(8) = class'KFMod.DamTypeLAW'
+ alwaysScale(9) = class'KFMod.DamTypeLawRocketImpact'
+ alwaysScale(10) = class'KFMod.DamTypeFlameNade'
+ alwaysScale(11) = class'KFMod.DamTypeFlareRevolver'
+ alwaysScale(12) = class'KFMod.DamTypeFlareProjectileImpact'
+ alwaysScale(13) = class'KFMod.DamTypeBurned'
+ alwaysScale(14) = class'KFMod.DamTypeTrenchgun'
+ alwaysScale(15) = class'KFMod.DamTypeHuskGun'
+ alwaysScale(16) = class'KFMod.DamTypeCrossbow'
+ alwaysScale(17) = class'KFMod.DamTypeCrossbowHeadShot'
+ alwaysScale(18) = class'KFMod.DamTypeM99SniperRifle'
+ alwaysScale(19) = class'KFMod.DamTypeM99HeadShot'
+ alwaysScale(20) = class'KFMod.DamTypeShotgun'
+ alwaysScale(21) = class'KFMod.DamTypeNailGun'
+ alwaysScale(22) = class'KFMod.DamTypeDBShotgun'
+ alwaysScale(23) = class'KFMod.DamTypeKSGShotgun'
+ alwaysScale(24) = class'KFMod.DamTypeBenelli'
+ alwaysScale(25) = class'KFMod.DamTypeSPGrenade'
+ alwaysScale(26) = class'KFMod.DamTypeSPGrenadeImpact'
+ alwaysScale(27) = class'KFMod.DamTypeSeekerSixRocket'
+ alwaysScale(28) = class'KFMod.DamTypeSeekerRocketImpact'
+ alwaysScale(29) = class'KFMod.DamTypeSealSquealExplosion'
+ alwaysScale(30) = class'KFMod.DamTypeRocketImpact'
+ alwaysScale(31) = class'KFMod.DamTypeBlowerThrower'
+ alwaysScale(32) = class'KFMod.DamTypeSPShotgun'
+ alwaysScale(33) = class'KFMod.DamTypeZEDGun'
+ alwaysScale(34) = class'KFMod.DamTypeZEDGunMKII'
+}
\ No newline at end of file
diff --git a/sources/FixInfiniteNades/FixInfiniteNades.uc b/sources/FixInfiniteNades/FixInfiniteNades.uc
index 3d46a6e..c01602e 100644
--- a/sources/FixInfiniteNades/FixInfiniteNades.uc
+++ b/sources/FixInfiniteNades/FixInfiniteNades.uc
@@ -1,8 +1,6 @@
- /**
- * This feature fixes a vulnerability in a code of `Frag` that can allow
- * player to throw grenades even when he no longer has any.
- * There's also no cooldowns on the throw, which can lead to a server crash.
- * Copyright 2019 Anton Tarasenko
+/**
+ * Config object for `FixInfiniteNades_Feature`.
+ * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -19,235 +17,34 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class FixInfiniteNades extends Feature
+class FixInfiniteNades extends FeatureConfig
+ perobjectconfig
config(AcediaFixes);
-/**
- * It is possible to call `ServerThrow` function from client,
- * forcing it to get executed on a server. This function consumes the grenade
- * ammo and spawns a nade, but it doesn't check if player had any grenade ammo
- * in the first place, allowing you him to throw however many grenades
- * he wants. Moreover, unlike a regular throwing method, calling this function
- * allows to spawn many grenades without any delay,
- * which can lead to a server crash.
- *
- * This fix tracks every instance of `Frag` weapon that's responsible for
- * throwing grenades and records how much ammo they have have.
- * This is necessary, because whatever means we use, when we get a say in
- * preventing grenade from spawning the ammo was already reduced.
- * This means that we can't distinguished between a player abusing a bug by
- * throwing grenade when he doesn't have necessary ammo and player throwing
- * his last nade, as in both cases current ammo visible to us will be 0.
- * Then, before every nade throw, it checks if player has enough ammo and
- * blocks grenade from spawning if he doesn't.
- * We change a `FireModeClass[0]` from `FragFire` to `FixedFragFire` and
- * only call `super.DoFireEffect()` if we decide spawning grenade
- * should be allowed. The side effect is a change in server's `FireModeClass`.
- */
-
-// Setting this flag to `true` will allow to throw grenades by calling
-// `ServerThrow()` directly, as long as player has necessary ammo.
-// This can allow some players to throw grenades much quicker than intended,
-// so if you wish to prevent it, keep this flag set to `false`.
-var private config const bool ignoreTossFlags;
-
-// Set to `true` when this fix is getting disabled to avoid replacing the
-// fire class again.
-var private bool shuttingDown;
-
-// Records how much ammo given frag grenade (`Frag`) has.
-struct FragAmmoRecord
-{
- // Reference to `Frag`
- var public NativeActorRef fragReference;
- var public int amount;
-};
-var private array ammoRecords;
-
-protected function OnEnabled()
-{
- local Frag nextFrag;
- local LevelInfo level;
- level = _.unreal.GetLevel();
- _.unreal.OnTick(self).connect = Tick;
- shuttingDown = false;
- // Find all frags, that spawned when this fix wasn't running.
- foreach level.DynamicActors(class'KFMod.Frag', nextFrag) {
- RegisterFrag(nextFrag);
- }
- RecreateFrags();
-}
-
-protected function OnDisabled()
-{
- _.unreal.OnTick(self).Disconnect();
- shuttingDown = true;
- RecreateFrags();
- ammoRecords.length = 0;
-}
+var public config bool ignoreTossFlags;
-// Returns `true` when this feature is in the process of shutting down,
-// which means nades' fire class should not be replaced.
-public final function bool IsShuttingDown()
+protected function AssociativeArray ToData()
{
- return shuttingDown;
+ local AssociativeArray data;
+ data = __().collections.EmptyAssociativeArray();
+ data.SetBool(P("ignoreTossFlags"), ignoreTossFlags, true);
+ return data;
}
-// Returns index of the connection 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 GetAmmoIndex(Frag fragToCheck)
+protected function FromData(AssociativeArray source)
{
- local int i;
- if (fragToCheck == none) return -1;
-
- for (i = 0; i < ammoRecords.length; i += 1)
- {
- if (ammoRecords[i].fragReference.Get() == fragToCheck) {
- return i;
- }
+ if (source != none) {
+ ignoreTossFlags = source.GetBool(P("ignoreTossFlags"), true);
}
- return -1;
}
-// Recreates all the `Frag` actors, to change their fire mode mid-game.
-private final function RecreateFrags()
+protected function DefaultIt()
{
- local int i;
- local float maxAmmo, currentAmmo;
- local Frag newFrag, oldFrag;
- local Pawn fragOwner;
- local array oldRecords;
- oldRecords = ammoRecords;
- for (i = 0; i < oldRecords.length; i += 1)
- {
- // Check if we even need to recreate that instance of `Frag`
- oldFrag = Frag(oldRecords[i].fragReference.Get());
- oldRecords[i].fragReference.FreeSelf();
- if (oldFrag == none) continue;
- fragOwner = oldFrag.instigator;
- if (fragOwner == none) continue;
- // Recreate
- oldFrag.Destroy();
- fragOwner.CreateInventory("KFMod.Frag");
- newFrag = GetPawnFrag(fragOwner);
- // Restore ammo amount
- if (newFrag != none)
- {
- newFrag.GetAmmoCount(maxAmmo, currentAmmo);
- newFrag.AddAmmo(oldRecords[i].amount - Int(currentAmmo), 0);
- }
- }
-}
-
-// Utility function to help find a `Frag` instance in a given pawn's inventory.
-static private final function Frag GetPawnFrag(Pawn pawnWithFrag)
-{
- local Frag foundFrag;
- local Inventory invIter;
- if (pawnWithFrag == none) return none;
- invIter = pawnWithFrag.inventory;
- while (invIter != none)
- {
- foundFrag = Frag(invIter);
- if (foundFrag != none) {
- return foundFrag;
- }
- invIter = invIter.inventory;
- }
- return none;
-}
-
-// Utility function for extracting current ammo amount from a frag class.
-private final function int GetFragAmmo(Frag fragReference)
-{
- local float maxAmmo;
- local float currentAmmo;
- if (fragReference == none) return 0;
-
- fragReference.GetAmmoCount(maxAmmo, currentAmmo);
- return Int(currentAmmo);
-}
-
-// Attempts to add new `Frag` instance to our records.
-public final function RegisterFrag(Frag newFrag)
-{
- local int index;
- local FragAmmoRecord newRecord;
- index = GetAmmoIndex(newFrag);
- if (index >= 0) return;
-
- newRecord.fragReference = _.unreal.ActorRef(newFrag);
- newRecord.amount = GetFragAmmo(newFrag);
- ammoRecords[ammoRecords.length] = newRecord;
-}
-
-// This function tells our fix that there was a nade throw and we should
-// reduce current `Frag` ammo in our records.
-// Returns `true` if we had ammo for that, and `false` if we didn't.
-public final function bool RegisterNadeThrow(Frag relevantFrag)
-{
- if (CanThrowGrenade(relevantFrag))
- {
- ReduceGrenades(relevantFrag);
- return true;
- }
- return false;
-}
-
-// Can we throw grenade according to our rules?
-// A throw can be prevented if:
-// - we think that player doesn't have necessary ammo;
-// - Player isn't currently `tossing` a nade,
-// meaning it was a direct call of `ServerThrow`.
-private final function bool CanThrowGrenade(Frag fragToCheck)
-{
- local int index;
- // Nothing to check
- if (fragToCheck == none) return false;
- // No ammo
- index = GetAmmoIndex(fragToCheck);
- if (index < 0) return false;
- if (ammoRecords[index].amount <= 0) return false;
- // Not tossing
- if (ignoreTossFlags) return true;
- if (!fragToCheck.bTossActive || fragToCheck.bTossSpawned) return false;
- return true;
-}
-
-// Reduces recorded amount of ammo in our records for the given nade.
-private final function ReduceGrenades(Frag relevantFrag)
-{
- local int index;
- index = GetAmmoIndex(relevantFrag);
- if (index < 0) return;
- ammoRecords[index].amount -= 1;
-}
-
-private function Tick(float delta, float timeDilationCoefficient)
-{
- local int i;
- local Frag nextFrag;
- // Update our ammo records with current, correct data.
- while (i < ammoRecords.length)
- {
- nextFrag = Frag(ammoRecords[i].fragReference.Get());
- if (nextFrag != none)
- {
- ammoRecords[i].amount = GetFragAmmo(nextFrag);
- i += 1;
- }
- else
- {
- ammoRecords[i].fragReference.FreeSelf();
- ammoRecords.Remove(i, 1);
- }
- }
+ ignoreTossFlags = true;
}
defaultproperties
{
+ configName = "AcediaFixes"
ignoreTossFlags = true
- // Listeners
- requiredListeners(0) = class'MutatorListener_FixInfiniteNades'
}
\ No newline at end of file
diff --git a/sources/FixInfiniteNades/FixInfiniteNades_Feature.uc b/sources/FixInfiniteNades/FixInfiniteNades_Feature.uc
new file mode 100644
index 0000000..af90ebb
--- /dev/null
+++ b/sources/FixInfiniteNades/FixInfiniteNades_Feature.uc
@@ -0,0 +1,273 @@
+ /**
+ * This feature fixes a vulnerability in a code of `Frag` that can allow
+ * player to throw grenades even when he no longer has any.
+ * There's also no cooldowns on the throw, which can lead to a server crash.
+ * 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 FixInfiniteNades_Feature extends Feature;
+
+/**
+ * It is possible to call `ServerThrow` function from client,
+ * forcing it to get executed on a server. This function consumes the grenade
+ * ammo and spawns a nade, but it doesn't check if player had any grenade ammo
+ * in the first place, allowing you him to throw however many grenades
+ * he wants. Moreover, unlike a regular throwing method, calling this function
+ * allows to spawn many grenades without any delay,
+ * which can lead to a server crash.
+ *
+ * This fix tracks every instance of `Frag` weapon that's responsible for
+ * throwing grenades and records how much ammo they have have.
+ * This is necessary, because whatever means we use, when we get a say in
+ * preventing grenade from spawning the ammo was already reduced.
+ * This means that we can't distinguished between a player abusing a bug by
+ * throwing grenade when he doesn't have necessary ammo and player throwing
+ * his last nade, as in both cases current ammo visible to us will be 0.
+ * Then, before every nade throw, it checks if player has enough ammo and
+ * blocks grenade from spawning if he doesn't.
+ * We change a `FireModeClass[0]` from `FragFire` to `FixedFragFire` and
+ * only call `super.DoFireEffect()` if we decide spawning grenade
+ * should be allowed. The side effect is a change in server's `FireModeClass`.
+ */
+
+// Setting this flag to `true` will allow to throw grenades by calling
+// `ServerThrow()` directly, as long as player has necessary ammo.
+// This can allow some players to throw grenades much quicker than intended,
+// so if you wish to prevent it, keep this flag set to `false`.
+var private /*config*/ bool ignoreTossFlags;
+
+// Set to `true` when this fix is getting disabled to avoid replacing the
+// fire class again.
+var private bool shuttingDown;
+
+// Records how much ammo given frag grenade (`Frag`) has.
+struct FragAmmoRecord
+{
+ // Reference to `Frag`
+ var public NativeActorRef fragReference;
+ var public int amount;
+};
+var private array ammoRecords;
+
+protected function OnEnabled()
+{
+ local Frag nextFrag;
+ local LevelInfo level;
+ level = _.unreal.GetLevel();
+ _.unreal.OnTick(self).connect = Tick;
+ _.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement;
+ shuttingDown = false;
+ // Find all frags, that spawned when this fix wasn't running.
+ foreach level.DynamicActors(class'KFMod.Frag', nextFrag) {
+ RegisterFrag(nextFrag);
+ }
+ RecreateFrags();
+}
+
+protected function OnDisabled()
+{
+ _.unreal.OnTick(self).Disconnect();
+ _.unreal.mutator.OnCheckReplacement(self).Disconnect();
+ shuttingDown = true;
+ RecreateFrags();
+ ammoRecords.length = 0;
+}
+
+protected function SwapConfig(FeatureConfig config)
+{
+ local FixInfiniteNades newConfig;
+ newConfig = FixInfiniteNades(config);
+ if (newConfig == none) {
+ return;
+ }
+ ignoreTossFlags = newConfig.ignoreTossFlags;
+}
+
+private function bool CheckReplacement(Actor other, out byte isSuperRelevant)
+{
+ local Frag relevantFrag;
+ if (shuttingDown) {
+ return true;
+ }
+ // Handle detecting new frag (weapons that allows to throw nades)
+ relevantFrag = Frag(other);
+ if (relevantFrag != none)
+ {
+ RegisterFrag(relevantFrag);
+ relevantFrag.FireModeClass[0] = class'FixedFragFire';
+ return true;
+ }
+ return true;
+}
+
+// Returns index of the connection 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 GetAmmoIndex(Frag fragToCheck)
+{
+ local int i;
+ if (fragToCheck == none) return -1;
+
+ for (i = 0; i < ammoRecords.length; i += 1)
+ {
+ if (ammoRecords[i].fragReference.Get() == fragToCheck) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+// Recreates all the `Frag` actors, to change their fire mode mid-game.
+private final function RecreateFrags()
+{
+ local int i;
+ local float maxAmmo, currentAmmo;
+ local Frag newFrag, oldFrag;
+ local Pawn fragOwner;
+ local array oldRecords;
+ oldRecords = ammoRecords;
+ for (i = 0; i < oldRecords.length; i += 1)
+ {
+ // Check if we even need to recreate that instance of `Frag`
+ oldFrag = Frag(oldRecords[i].fragReference.Get());
+ oldRecords[i].fragReference.FreeSelf();
+ if (oldFrag == none) continue;
+ fragOwner = oldFrag.instigator;
+ if (fragOwner == none) continue;
+ // Recreate
+ oldFrag.Destroy();
+ fragOwner.CreateInventory("KFMod.Frag");
+ newFrag = GetPawnFrag(fragOwner);
+ // Restore ammo amount
+ if (newFrag != none)
+ {
+ newFrag.GetAmmoCount(maxAmmo, currentAmmo);
+ newFrag.AddAmmo(oldRecords[i].amount - Int(currentAmmo), 0);
+ }
+ }
+}
+
+// Utility function to help find a `Frag` instance in a given pawn's inventory.
+static private final function Frag GetPawnFrag(Pawn pawnWithFrag)
+{
+ local Frag foundFrag;
+ local Inventory invIter;
+ if (pawnWithFrag == none) return none;
+ invIter = pawnWithFrag.inventory;
+ while (invIter != none)
+ {
+ foundFrag = Frag(invIter);
+ if (foundFrag != none) {
+ return foundFrag;
+ }
+ invIter = invIter.inventory;
+ }
+ return none;
+}
+
+// Utility function for extracting current ammo amount from a frag class.
+private final function int GetFragAmmo(Frag fragReference)
+{
+ local float maxAmmo;
+ local float currentAmmo;
+ if (fragReference == none) return 0;
+
+ fragReference.GetAmmoCount(maxAmmo, currentAmmo);
+ return Int(currentAmmo);
+}
+
+// Attempts to add new `Frag` instance to our records.
+public final function RegisterFrag(Frag newFrag)
+{
+ local int index;
+ local FragAmmoRecord newRecord;
+ index = GetAmmoIndex(newFrag);
+ if (index >= 0) return;
+
+ newRecord.fragReference = _.unreal.ActorRef(newFrag);
+ newRecord.amount = GetFragAmmo(newFrag);
+ ammoRecords[ammoRecords.length] = newRecord;
+}
+
+// This function tells our fix that there was a nade throw and we should
+// reduce current `Frag` ammo in our records.
+// Returns `true` if we had ammo for that, and `false` if we didn't.
+public final function bool RegisterNadeThrow(Frag relevantFrag)
+{
+ if (CanThrowGrenade(relevantFrag))
+ {
+ ReduceGrenades(relevantFrag);
+ return true;
+ }
+ return false;
+}
+
+// Can we throw grenade according to our rules?
+// A throw can be prevented if:
+// - we think that player doesn't have necessary ammo;
+// - Player isn't currently `tossing` a nade,
+// meaning it was a direct call of `ServerThrow`.
+private final function bool CanThrowGrenade(Frag fragToCheck)
+{
+ local int index;
+ // Nothing to check
+ if (fragToCheck == none) return false;
+ // No ammo
+ index = GetAmmoIndex(fragToCheck);
+ if (index < 0) return false;
+ if (ammoRecords[index].amount <= 0) return false;
+ // Not tossing
+ if (ignoreTossFlags) return true;
+ if (!fragToCheck.bTossActive || fragToCheck.bTossSpawned) return false;
+ return true;
+}
+
+// Reduces recorded amount of ammo in our records for the given nade.
+private final function ReduceGrenades(Frag relevantFrag)
+{
+ local int index;
+ index = GetAmmoIndex(relevantFrag);
+ if (index < 0) return;
+ ammoRecords[index].amount -= 1;
+}
+
+private function Tick(float delta, float timeDilationCoefficient)
+{
+ local int i;
+ local Frag nextFrag;
+ // Update our ammo records with current, correct data.
+ while (i < ammoRecords.length)
+ {
+ nextFrag = Frag(ammoRecords[i].fragReference.Get());
+ if (nextFrag != none)
+ {
+ ammoRecords[i].amount = GetFragAmmo(nextFrag);
+ i += 1;
+ }
+ else
+ {
+ ammoRecords[i].fragReference.FreeSelf();
+ ammoRecords.Remove(i, 1);
+ }
+ }
+}
+
+defaultproperties
+{
+ configClass = class'FixInfiniteNades'
+ ignoreTossFlags = true
+}
\ No newline at end of file
diff --git a/sources/FixInfiniteNades/FixedFragFire.uc b/sources/FixInfiniteNades/FixedFragFire.uc
index 702696f..fe7cba6 100644
--- a/sources/FixInfiniteNades/FixedFragFire.uc
+++ b/sources/FixInfiniteNades/FixedFragFire.uc
@@ -23,8 +23,9 @@ class FixedFragFire extends KFMod.FragFire;
function DoFireEffect()
{
- local FixInfiniteNades nadeFix;
- nadeFix = FixInfiniteNades(class'FixInfiniteNades'.static.GetInstance());
+ local FixInfiniteNades_Feature nadeFix;
+ nadeFix = FixInfiniteNades_Feature(
+ class'FixInfiniteNades_Feature'.static.GetInstance());
if (nadeFix == none || nadeFix.RegisterNadeThrow(Frag(weapon))) {
super.DoFireEffect();
}
diff --git a/sources/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc b/sources/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc
deleted file mode 100644
index 255b1c7..0000000
--- a/sources/FixInfiniteNades/MutatorListener_FixInfiniteNades.uc
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Overloaded mutator events listener to catch
- * new `Frag` weapons and `Nade` projectiles.
- * 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 MutatorListener_FixInfiniteNades extends MutatorListenerBase
- abstract;
-
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- local Frag relevantFrag;
- local FixInfiniteNades nadeFix;
- nadeFix = FixInfiniteNades(class'FixInfiniteNades'.static.GetInstance());
- if (nadeFix == none) return true;
- if (nadeFix.IsShuttingDown()) return true;
-
- // Handle detecting new frag (weapons that allows to throw nades)
- relevantFrag = Frag(other);
- if (relevantFrag != none)
- {
- nadeFix.RegisterFrag(relevantFrag);
- relevantFrag.FireModeClass[0] = class'FixedFragFire';
- return true;
- }
- return true;
-}
-
-defaultproperties
-{
-}
\ No newline at end of file
diff --git a/sources/FixInventoryAbuse/FixInventoryAbuse.uc b/sources/FixInventoryAbuse/FixInventoryAbuse.uc
index 083f8fe..f8c7ac9 100644
--- a/sources/FixInventoryAbuse/FixInventoryAbuse.uc
+++ b/sources/FixInventoryAbuse/FixInventoryAbuse.uc
@@ -1,16 +1,6 @@
/**
- * This feature addressed two inventory issues:
- * 1. Players carrying amount of weapons that shouldn't be allowed by the
- * weight limit.
- * 2. Players carrying two variants of the same gun.
- * For example carrying both M32 and camo M32.
- * Single and dual version of the same weapon are also considered
- * the same gun, so you can't carry both MK23 and dual MK23 or
- * dual handcannons and golden handcannon.
- *
- * It fixes them by doing repeated checks to find violations of those rules
- * and destroys all droppable weapons of people that use this exploit.
- * Copyright 2020 Anton Tarasenko
+ * Config object for `FixInventoryAbuse_Feature`.
+ * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -27,192 +17,91 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class FixInventoryAbuse extends Feature
- config(AcediaFixes);
-
-// How often (in seconds) should we do our inventory validations?
-// We shouldn't really worry about performance, but there's also no need to
-// do this check too often.
-var private config const float checkInterval;
-
-var private Timer checkTimer;
-
-struct DualiesPair
-{
- var class single;
- var class dual;
-};
-// For this fix to properly work, this array must contain an entry for
-// every dual weapon in the game (like pistols, with single and dual versions).
-// It's made configurable in case of custom dual weapons.
-var private config const array dualiesClasses;
-
-protected function OnEnabled()
-{
- local float actualInterval;
- actualInterval = checkInterval;
- if (actualInterval <= 0) {
- actualInterval = 0.25;
- }
- checkTimer = _.time.StartTimer(actualInterval, true);
- checkTimer.OnElapsed(self).connect = Timer;
-}
+class FixInventoryAbuse extends FeatureConfig
+ perobjectconfig
+ config(AcediaFixes)
+ dependson(FixInventoryAbuse_Feature);
-protected function OnDisabled()
-{
- _.memory.Free(checkTimer);
-}
-
-// Did player with this controller contribute to the latest dosh generation?
-private final function bool IsWeightLimitViolated(KFHumanPawn playerPawn)
-{
- if (playerPawn == none) return false;
- return (playerPawn.currentWeight > playerPawn.maxCarryWeight);
-}
+var public config float checkInterval;
+var public config array dualiesClasses;
-// Returns a root pickup class.
-// For non-dual weapons, root class is defined as either:
-// 1. the first variant (reskin), if there are variants for that weapon;
-// 2. and as the class itself, if there are no variants.
-// For dual weapons (all dual pistols) root class is defined as
-// a root of their single version.
-// This definition is useful because:
-// ~ Vanilla game rules are such that player can only have two weapons
-// in the inventory if they have different roots;
-// ~ Root is easy to find.
-private final function class GetRootPickupClass(KFWeapon weapon)
+protected function AssociativeArray ToData()
{
- local int i;
- local class root;
- if (weapon == none) return none;
- // Start with a pickup of the given weapons
- root = class(weapon.default.pickupClass);
- if (root == none) return none;
-
- // In case it's a dual version - find corresponding single pickup class
- // (it's root would be the same).
+ local int i;
+ local DynamicArray pairsArray;
+ local AssociativeArray data, pair;
+ data = _.collections.EmptyAssociativeArray();
+ data.SetFloat(P("checkInterval"), checkInterval, true);
+ pairsArray = _.collections.EmptyDynamicArray();
for (i = 0; i < dualiesClasses.length; i += 1)
{
- if (dualiesClasses[i].dual == root)
- {
- root = dualiesClasses[i].single;
- break;
- }
- }
- // Take either first variant class or the class itself, -
- // it's going to be root by definition.
- if (root.default.variantClasses.length > 0)
- {
- root = class(root.default.variantClasses[0]);
+ pair = _.collections.EmptyAssociativeArray();
+ pair.SetItem( P("single"),
+ _.text.FromString(string(dualiesClasses[i].single)));
+ pair.SetItem( P("dual"),
+ _.text.FromString(string(dualiesClasses[i].dual)));
+ pairsArray.AddItem(pair);
}
- return root;
+ data.SetItem(P("dualiesClasses"), pairsArray);
+ return data;
}
-// Returns `true` if passed pawn has two weapons that are just variants of
-// each other (they have the same root, see `GetRootPickupClass()`).
-private final function bool HasDuplicateGuns(KFHumanPawn playerPawn)
+protected function FromData(AssociativeArray source)
{
- local int i, j;
- local Inventory inv;
- local KFWeapon nextWeapon;
- local class rootClass;
- local array< class > rootList;
- if (playerPawn == none) return false;
-
- // First find a root for every weapon in the pawn's inventory.
- for (inv = playerPawn.inventory; inv != none; inv = inv.inventory)
- {
- nextWeapon = KFWeapon(inv);
- if (nextWeapon == none) continue;
- if (nextWeapon.bKFNeverThrow) continue;
- rootClass = GetRootPickupClass(nextWeapon);
- if (rootClass != none) {
- rootList[rootList.length] = rootClass;
- }
+ local int i;
+ local DynamicArray pairsArray;
+ local AssociativeArray loadedPair;
+ local FixInventoryAbuse_Feature.DualiesPair newPair;
+ if (source == none) {
+ return;
}
- // Then just check obtained roots for duplicates.
- for (i = 0; i < rootList.length; i += 1)
- {
- for (j = i + 1; j < rootList.length; j += 1)
- {
- if (rootList[i] == rootList[j]) {
- return true;
- }
- }
+ checkInterval = source.GetFloat(P("checkInterval"), 0.25);
+ pairsArray = source.GetDynamicArray(P("dualiesClasses"));
+ dualiesClasses.length = 0;
+ if (pairsArray == none) {
+ return;
}
- return false;
-}
-
-private final function Vector DropWeapon(KFWeapon weaponToDrop)
-{
- local Vector x, y, z;
- local Vector weaponVelocity;
- local Vector dropLocation;
- local KFHumanPawn playerPawn;
- if (weaponToDrop == none) return Vect(0, 0, 0);
- playerPawn = KFHumanPawn(weaponToDrop.instigator);
- if (playerPawn == none) return Vect(0, 0, 0);
-
- // Calculations from `PlayerController.ServerThrowWeapon()`
- weaponVelocity = Vector(playerPawn.GetViewRotation());
- weaponVelocity *= (playerPawn.velocity dot weaponVelocity) + 150;
- weaponVelocity += Vect(0, 0, 100);
- // Calculations from `Pawn.TossWeapon()`
- GetAxes(playerPawn.rotation, x, y, z);
- dropLocation = playerPawn.location + 0.8 * playerPawn.collisionRadius * x -
- 0.5 * playerPawn.collisionRadius * y;
- // Do the drop
- weaponToDrop.velocity = weaponVelocity;
- weaponToDrop.DropFrom(dropLocation);
-}
-
-// Kill the gun devil!
-private final function DropEverything(KFHumanPawn playerPawn)
-{
- local int i;
- local Inventory inv;
- local KFWeapon nextWeapon;
- local array weaponList;
- if (playerPawn == none) return;
- // Going through the linked list while removing items can be tricky,
- // so just find all weapons first.
- for (inv = playerPawn.inventory; inv != none; inv = inv.inventory)
+ for (i = 0; i < pairsArray.GetLength(); i += 1)
{
- nextWeapon = KFWeapon(inv);
- if (nextWeapon == none) continue;
- if (nextWeapon.bKFNeverThrow) continue;
- weaponList[weaponList.length] = nextWeapon;
- }
- // And destroy them later.
- for(i = 0; i < weaponList.length; i += 1) {
- DropWeapon(weaponList[i]);
+ loadedPair = pairsArray.GetAssociativeArray(i);
+ if (loadedPair == none) continue;
+
+ newPair.single = class(
+ _.memory.LoadClass(loadedPair.GetText(P("single"))) );
+ newPair.dual = class(
+ _.memory.LoadClass(loadedPair.GetText(P("dual"))) );
+ dualiesClasses[dualiesClasses.length] = newPair;
}
}
-private function Timer(Timer source)
+protected function DefaultIt()
{
- local int i;
- local KFHumanPawn nextPawn;
- local ConnectionService service;
- local array connections;
- service = ConnectionService(class'ConnectionService'.static.GetInstance());
- if (service == none) return;
-
- connections = service.GetActiveConnections();
- for (i = 0; i < connections.length; i += 1)
- {
- nextPawn = none;
- if (connections[i].controllerReference != none) {
- nextPawn = KFHumanPawn(connections[i].controllerReference.pawn);
- }
- if (IsWeightLimitViolated(nextPawn) || HasDuplicateGuns(nextPawn)) {
- DropEverything(nextPawn);
- }
- }
+ local FixInventoryAbuse_Feature.DualiesPair newPair;
+ checkInterval = 0.25;
+ dualiesClasses.length = 0;
+ newPair.single = class'KFMod.SinglePickup';
+ newPair.dual = class'KFMod.DualiesPickup';
+ dualiesClasses[dualiesClasses.length] = newPair;
+ newPair.single = class'KFMod.Magnum44Pickup';
+ newPair.dual = class'KFMod.Dual44MagnumPickup';
+ dualiesClasses[dualiesClasses.length] = newPair;
+ newPair.single = class'KFMod.MK23Pickup';
+ newPair.dual = class'KFMod.DualMK23Pickup';
+ dualiesClasses[dualiesClasses.length] = newPair;
+ newPair.single = class'KFMod.DeaglePickup';
+ newPair.dual = class'KFMod.DualDeaglePickup';
+ dualiesClasses[dualiesClasses.length] = newPair;
+ newPair.single = class'KFMod.GoldenDeaglePickup';
+ newPair.dual = class'KFMod.GoldenDualDeaglePickup';
+ dualiesClasses[dualiesClasses.length] = newPair;
+ newPair.single = class'KFMod.FlareRevolverPickup';
+ newPair.dual = class'KFMod.DualFlareRevolverPickup';
+ dualiesClasses[dualiesClasses.length] = newPair;
}
defaultproperties
{
+ configName = "AcediaFixes"
checkInterval = 0.25
dualiesClasses(0)=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup')
dualiesClasses(1)=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup')
diff --git a/sources/FixInventoryAbuse/FixInventoryAbuse_Feature.uc b/sources/FixInventoryAbuse/FixInventoryAbuse_Feature.uc
new file mode 100644
index 0000000..b277302
--- /dev/null
+++ b/sources/FixInventoryAbuse/FixInventoryAbuse_Feature.uc
@@ -0,0 +1,237 @@
+/**
+ * This feature addressed two inventory issues:
+ * 1. Players carrying amount of weapons that shouldn't be allowed by the
+ * weight limit.
+ * 2. Players carrying two variants of the same gun.
+ * For example carrying both M32 and camo M32.
+ * Single and dual version of the same weapon are also considered
+ * the same gun, so you can't carry both MK23 and dual MK23 or
+ * dual handcannons and golden handcannon.
+ *
+ * It fixes them by doing repeated checks to find violations of those rules
+ * and destroys all droppable weapons of people that use this exploit.
+ * 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 .
+ */
+class FixInventoryAbuse_Feature extends Feature;
+
+var private Timer checkTimer;
+
+// How often (in seconds) should we do our inventory validations?
+// We shouldn't really worry about performance, but there's also no need to
+// do this check too often.
+var private /*config*/ float checkInterval;
+
+struct DualiesPair
+{
+ var class single;
+ var class dual;
+};
+// For this fix to properly work, this array must contain an entry for
+// every dual weapon in the game (like pistols, with single and dual versions).
+// It's made configurable in case of custom dual weapons.
+var private /*config*/ array dualiesClasses;
+
+protected function OnEnabled()
+{
+ local float actualInterval;
+ actualInterval = checkInterval;
+ if (actualInterval <= 0) {
+ actualInterval = 0.25;
+ }
+ checkTimer = _.time.StartTimer(actualInterval, true);
+ checkTimer.OnElapsed(self).connect = Timer;
+}
+
+protected function OnDisabled()
+{
+ _.memory.Free(checkTimer);
+}
+
+protected function SwapConfig(FeatureConfig config)
+{
+ local FixInventoryAbuse newConfig;
+ newConfig = FixInventoryAbuse(config);
+ if (newConfig == none) {
+ return;
+ }
+ checkInterval = newConfig.checkInterval;
+ dualiesClasses = newConfig.dualiesClasses;
+ if (checkTimer != none) {
+ checkTimer.SetInterval(checkInterval);
+ }
+}
+
+// Did player with this controller contribute to the latest dosh generation?
+private final function bool IsWeightLimitViolated(KFHumanPawn playerPawn)
+{
+ if (playerPawn == none) return false;
+ return (playerPawn.currentWeight > playerPawn.maxCarryWeight);
+}
+
+// Returns a root pickup class.
+// For non-dual weapons, root class is defined as either:
+// 1. the first variant (reskin), if there are variants for that weapon;
+// 2. and as the class itself, if there are no variants.
+// For dual weapons (all dual pistols) root class is defined as
+// a root of their single version.
+// This definition is useful because:
+// ~ Vanilla game rules are such that player can only have two weapons
+// in the inventory if they have different roots;
+// ~ Root is easy to find.
+private final function class GetRootPickupClass(KFWeapon weapon)
+{
+ local int i;
+ local class root;
+ if (weapon == none) return none;
+ // Start with a pickup of the given weapons
+ root = class(weapon.default.pickupClass);
+ if (root == none) return none;
+
+ // In case it's a dual version - find corresponding single pickup class
+ // (it's root would be the same).
+ for (i = 0; i < dualiesClasses.length; i += 1)
+ {
+ if (dualiesClasses[i].dual == root)
+ {
+ root = dualiesClasses[i].single;
+ break;
+ }
+ }
+ // Take either first variant class or the class itself, -
+ // it's going to be root by definition.
+ if (root.default.variantClasses.length > 0)
+ {
+ root = class(root.default.variantClasses[0]);
+ }
+ return root;
+}
+
+// Returns `true` if passed pawn has two weapons that are just variants of
+// each other (they have the same root, see `GetRootPickupClass()`).
+private final function bool HasDuplicateGuns(KFHumanPawn playerPawn)
+{
+ local int i, j;
+ local Inventory inv;
+ local KFWeapon nextWeapon;
+ local class rootClass;
+ local array< class > rootList;
+ if (playerPawn == none) return false;
+
+ // First find a root for every weapon in the pawn's inventory.
+ for (inv = playerPawn.inventory; inv != none; inv = inv.inventory)
+ {
+ nextWeapon = KFWeapon(inv);
+ if (nextWeapon == none) continue;
+ if (nextWeapon.bKFNeverThrow) continue;
+ rootClass = GetRootPickupClass(nextWeapon);
+ if (rootClass != none) {
+ rootList[rootList.length] = rootClass;
+ }
+ }
+ // Then just check obtained roots for duplicates.
+ for (i = 0; i < rootList.length; i += 1)
+ {
+ for (j = i + 1; j < rootList.length; j += 1)
+ {
+ if (rootList[i] == rootList[j]) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+private final function Vector DropWeapon(KFWeapon weaponToDrop)
+{
+ local Vector x, y, z;
+ local Vector weaponVelocity;
+ local Vector dropLocation;
+ local KFHumanPawn playerPawn;
+ if (weaponToDrop == none) return Vect(0, 0, 0);
+ playerPawn = KFHumanPawn(weaponToDrop.instigator);
+ if (playerPawn == none) return Vect(0, 0, 0);
+
+ // Calculations from `PlayerController.ServerThrowWeapon()`
+ weaponVelocity = Vector(playerPawn.GetViewRotation());
+ weaponVelocity *= (playerPawn.velocity dot weaponVelocity) + 150;
+ weaponVelocity += Vect(0, 0, 100);
+ // Calculations from `Pawn.TossWeapon()`
+ GetAxes(playerPawn.rotation, x, y, z);
+ dropLocation = playerPawn.location + 0.8 * playerPawn.collisionRadius * x -
+ 0.5 * playerPawn.collisionRadius * y;
+ // Do the drop
+ weaponToDrop.velocity = weaponVelocity;
+ weaponToDrop.DropFrom(dropLocation);
+}
+
+// Kill the gun devil!
+private final function DropEverything(KFHumanPawn playerPawn)
+{
+ local int i;
+ local Inventory inv;
+ local KFWeapon nextWeapon;
+ local array weaponList;
+ if (playerPawn == none) return;
+ // Going through the linked list while removing items can be tricky,
+ // so just find all weapons first.
+ for (inv = playerPawn.inventory; inv != none; inv = inv.inventory)
+ {
+ nextWeapon = KFWeapon(inv);
+ if (nextWeapon == none) continue;
+ if (nextWeapon.bKFNeverThrow) continue;
+ weaponList[weaponList.length] = nextWeapon;
+ }
+ // And destroy them later.
+ for(i = 0; i < weaponList.length; i += 1) {
+ DropWeapon(weaponList[i]);
+ }
+}
+
+private function Timer(Timer source)
+{
+ local int i;
+ local KFHumanPawn nextPawn;
+ local ConnectionService service;
+ local array connections;
+ service = ConnectionService(class'ConnectionService'.static.GetInstance());
+ if (service == none) return;
+
+ connections = service.GetActiveConnections();
+ for (i = 0; i < connections.length; i += 1)
+ {
+ nextPawn = none;
+ if (connections[i].controllerReference != none) {
+ nextPawn = KFHumanPawn(connections[i].controllerReference.pawn);
+ }
+ if (IsWeightLimitViolated(nextPawn) || HasDuplicateGuns(nextPawn)) {
+ DropEverything(nextPawn);
+ }
+ }
+}
+
+defaultproperties
+{
+ configClass = class'FixInventoryAbuse'
+ checkInterval = 0.25
+ dualiesClasses(0)=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup')
+ dualiesClasses(1)=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup')
+ dualiesClasses(2)=(single=class'KFMod.MK23Pickup',dual=class'KFMod.DualMK23Pickup')
+ dualiesClasses(3)=(single=class'KFMod.DeaglePickup',dual=class'KFMod.DualDeaglePickup')
+ dualiesClasses(4)=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDualDeaglePickup')
+ dualiesClasses(5)=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup')
+}
\ No newline at end of file
diff --git a/sources/FixLogSpam/FixLogSpam.uc b/sources/FixLogSpam/FixLogSpam.uc
index 2773d9f..e21e956 100644
--- a/sources/FixLogSpam/FixLogSpam.uc
+++ b/sources/FixLogSpam/FixLogSpam.uc
@@ -1,16 +1,5 @@
/**
- * This feature fixes different instances of log spam by the killing floor
- * with various warnings and errors. Some of them have actual underlying bugs
- * that need to be fixed, but a lot seem to be just a byproduct of dead and
- * abandoned features or simple negligence.
- * Whatever the case, now that TWI will no longer make any new changes to
- * the game a lot of them do not serve any purpose and simply pollute
- * log files. We try to get rid of at least some of them.
- * Since changes we make do not actually have gameplay effect and
- * are more aimed at convenience of server owners, our philosophy with the
- * changes will be to avoid solutions that are way too "hacky" and prefer some
- * message spam getting through to the possibility of some unexpected gameplay
- * effects as far as vanilla game is concerned.
+ * Config object for `FixLogSpam_Feature`.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
@@ -28,44 +17,40 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class FixLogSpam extends Feature
+class FixLogSpam extends FeatureConfig
+ perobjectconfig
config(AcediaFixes);
-// This is responsible for fixing log spam due to picking up dropped
-// weapons without set `inventory` variable.
-var private config const bool fixPickupSpam;
-var private HelperPickup helperPickupSpam;
+var public config bool fixPickupSpam;
+var public config bool fixTraderSpam;
-var private config const bool fixTraderSpam;
-var private HelperTrader helperTraderSpam;
-
-protected function OnEnabled()
+protected function AssociativeArray ToData()
{
- if (fixPickupSpam) {
- helperPickupSpam = HelperPickup(_.memory.Allocate(class'HelperPickup'));
- }
- if (fixTraderSpam) {
- helperTraderSpam = HelperTrader(_.memory.Allocate(class'HelperTrader'));
- }
+ local AssociativeArray data;
+ data = __().collections.EmptyAssociativeArray();
+ data.SetBool(P("fixPickupSpam"), fixPickupSpam, true);
+ data.SetBool(P("fixTraderSpam"), fixTraderSpam, true);
+ return data;
}
-protected function OnDisabled()
+protected function FromData(AssociativeArray source)
{
- _.memory.Free(helperPickupSpam);
- helperPickupSpam = none;
- _.memory.Free(helperTraderSpam);
- helperTraderSpam = none;
+ if (source != none)
+ {
+ fixPickupSpam = source.GetBool(P("fixPickupSpam"), true);
+ fixTraderSpam = source.GetBool(P("fixTraderSpam"), true);
+ }
}
-public function Tick(float delta)
+protected function DefaultIt()
{
- if (helperPickupSpam != none) {
- helperPickupSpam.Tick();
- }
+ fixPickupSpam = true;
+ fixTraderSpam = true;
}
defaultproperties
{
+ configName = "AcediaFixes"
fixPickupSpam = true
fixTraderSpam = true
}
\ No newline at end of file
diff --git a/sources/FixLogSpam/FixLogSpam_Feature.uc b/sources/FixLogSpam/FixLogSpam_Feature.uc
new file mode 100644
index 0000000..0ed0ae9
--- /dev/null
+++ b/sources/FixLogSpam/FixLogSpam_Feature.uc
@@ -0,0 +1,93 @@
+/**
+ * This feature fixes different instances of log spam by the killing floor
+ * with various warnings and errors. Some of them have actual underlying bugs
+ * that need to be fixed, but a lot seem to be just a byproduct of dead and
+ * abandoned features or simple negligence.
+ * Whatever the case, now that TWI will no longer make any new changes to
+ * the game a lot of them do not serve any purpose and simply pollute
+ * log files. We try to get rid of at least some of them.
+ * Since changes we make do not actually have gameplay effect and
+ * are more aimed at convenience of server owners, our philosophy with the
+ * changes will be to avoid solutions that are way too "hacky" and prefer some
+ * message spam getting through to the possibility of some unexpected gameplay
+ * effects as far as vanilla game is concerned.
+ * Copyright 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 .
+ */
+class FixLogSpam_Feature extends Feature;
+
+// This is responsible for fixing log spam due to picking up dropped
+// weapons without set `inventory` variable.
+var private /*config*/ bool fixPickupSpam;
+var private HelperPickup helperPickupSpam;
+
+var private /*config*/ bool fixTraderSpam;
+var private HelperTrader helperTraderSpam;
+
+protected function OnEnabled()
+{
+ if (fixPickupSpam) {
+ helperPickupSpam = HelperPickup(_.memory.Allocate(class'HelperPickup'));
+ }
+ if (fixTraderSpam) {
+ helperTraderSpam = HelperTrader(_.memory.Allocate(class'HelperTrader'));
+ }
+}
+
+protected function OnDisabled()
+{
+ _.memory.Free(helperPickupSpam);
+ helperPickupSpam = none;
+ _.memory.Free(helperTraderSpam);
+ helperTraderSpam = none;
+}
+
+protected function SwapConfig(FeatureConfig config)
+{
+ local FixLogSpam newConfig;
+ newConfig = FixLogSpam(config);
+ if (newConfig == none) {
+ return;
+ }
+ // Pickup spam
+ fixPickupSpam = newConfig.fixPickupSpam;
+ if (fixPickupSpam && helperPickupSpam == none) {
+ helperPickupSpam = HelperPickup(_.memory.Allocate(class'HelperPickup'));
+ }
+ if (!fixPickupSpam && helperPickupSpam != none)
+ {
+ _.memory.Free(helperPickupSpam);
+ helperPickupSpam = none;
+ }
+ // Trader fixTraderSpam
+ fixTraderSpam = newConfig.fixTraderSpam;
+ if (fixTraderSpam && helperTraderSpam == none) {
+ helperTraderSpam = HelperTrader(_.memory.Allocate(class'HelperPickup'));
+ }
+ if (!fixTraderSpam && helperTraderSpam != none)
+ {
+ _.memory.Free(helperTraderSpam);
+ helperTraderSpam = none;
+ }
+}
+
+defaultproperties
+{
+ configClass = class'FixLogSpam'
+ fixPickupSpam = true
+ fixTraderSpam = true
+}
\ No newline at end of file
diff --git a/sources/FixLogSpam/SpamPickup/HelperPickup.uc b/sources/FixLogSpam/SpamPickup/HelperPickup.uc
index 57409bd..82e84b9 100644
--- a/sources/FixLogSpam/SpamPickup/HelperPickup.uc
+++ b/sources/FixLogSpam/SpamPickup/HelperPickup.uc
@@ -18,8 +18,7 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class HelperPickup extends AcediaObject
- config(AcediaFixes);
+class HelperPickup extends AcediaObject;
/**
* `KFWeaponPickup` class is responsible for spamming log with
@@ -90,7 +89,9 @@ protected function Constructor()
// (and force additional pickup fix update)
_.unreal.gameRules.OnOverridePickupQuery(self).connect = PickupQuery;
// To detect newly spawned pickups
- class'MutatorListener_FixLogSpam_Pickup'.static.SetActive(true);
+ _.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement;
+ // For updating pickups as soon as possible
+ _.unreal.OnTick(self).connect = Tick;
// Find all `KFWeaponPickup`s laying around on the map,
// so that we can fix preexisting ones too.
// But add them to pending list in a freaky case this `HealperPickup`
@@ -119,7 +120,33 @@ protected function Finalizer()
recordedPickups.length = 0;
pendingPickups.length = 0;
_.unreal.gameRules.OnOverridePickupQuery(self).Disconnect();
- class'MutatorListener_FixLogSpam_Pickup'.static.SetActive(false);
+ _.unreal.mutator.OnCheckReplacement(self).Disconnect();
+ _.unreal.OnTick(self).Disconnect();
+}
+
+function bool PickupQuery(
+ Pawn toucher,
+ Pickup touchedPickup,
+ out byte allowPickup)
+{
+ UpdatePickups();
+ return false;
+}
+
+private function bool CheckReplacement(Actor other, out byte isSuperRelevant)
+{
+ local KFWeaponPickup otherPickup;
+ otherPickup = KFWeaponPickup(other);
+ if (otherPickup != none) {
+ HandlePickup(otherPickup);
+ }
+ return true;
+}
+
+private function Tick(float delta, float timeDilationCoefficient)
+{
+ CleanRecordedPickups();
+ UpdatePickups();
}
public final static function HelperPickup GetInstance()
@@ -214,21 +241,6 @@ private final function UpdatePickups()
pendingPickups.length = 0;
}
-function bool PickupQuery(
- Pawn toucher,
- Pickup touchedPickup,
- out byte allowPickup)
-{
- UpdatePickups();
- return false;
-}
-
-public final function Tick()
-{
- CleanRecordedPickups();
- UpdatePickups();
-}
-
defaultproperties
{
}
\ No newline at end of file
diff --git a/sources/FixLogSpam/SpamPickup/MutatorListener_FixLogSpam_Pickup.uc b/sources/FixLogSpam/SpamPickup/MutatorListener_FixLogSpam_Pickup.uc
deleted file mode 100644
index f47ef5b..0000000
--- a/sources/FixLogSpam/SpamPickup/MutatorListener_FixLogSpam_Pickup.uc
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Overloaded mutator events listener to catch and, possibly,
- * prevent spawning `KFWeaponPickup` for fixing log spam related to them.
- * Copyright 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 .
- */
-class MutatorListener_FixLogSpam_Pickup extends MutatorListenerBase
- abstract;
-
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- local HelperPickup helper;
- local KFWeaponPickup otherPickup;
- otherPickup = KFWeaponPickup(other);
- if (otherPickup == none) return true;
- helper = class'HelperPickup'.static.GetInstance();
- if (helper == none) return true;
-
- helper.HandlePickup(otherPickup);
- return true;
-}
-
-defaultproperties
-{
- relatedEvents = class'MutatorEvents'
-}
\ No newline at end of file
diff --git a/sources/FixPipes/FixPipes.uc b/sources/FixPipes/FixPipes.uc
index e5a88e1..8dc7bb8 100644
--- a/sources/FixPipes/FixPipes.uc
+++ b/sources/FixPipes/FixPipes.uc
@@ -1,11 +1,5 @@
/**
- * This feature addresses several bugs related to pipe bombs:
- * 1. Gaining extra explosive damage by shooting pipes with
- * a high fire rate weapon;
- * 2. Other players exploding one's pipes;
- * 3. Corpses and story NPCs exploding nearby pipes;
- * 4. Pipes being stuck in places where they cannot detect nearby
- * enemies and, therefore, not exploding.
+ * Config object for `FixPipes_Feature`.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
@@ -23,428 +17,55 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class FixPipes extends Feature
+class FixPipes extends FeatureConfig
+ perobjectconfig
config(AcediaFixes);
-/**
- * There are two main culprits for the issues we are interested in:
- * `TakeDamage()` and `Timer()` methods. `TakeDamage()` lacks sufficient checks
- * for instigator allows for ways to damage it, causing explosion. It also
- * lacks any checks for whether pipe bom already exploded, allowing players to
- * damage it several time before it's destroyed, causing explosion every time.
- * `Timer()` method simply contains a porximity check for hostile pawns that
- * wrongfully detects certain actor (like `KF_StoryNPC` or players' corpses)
- * and cannot detect zeds around itself if pipe is placed in a way that
- * prevents it from having a direct line of sight towards zeds.
- * To fix our issues we have to somehow overload both of these methods,
- * which is impossible while keeping Acedia server-side. So we will have to
- * do some hacks.
- *
- * `TakeDamage()`. To override this method's behavior we will remove actor
- * collision from pipe bombs, preventing it from being directly damaged, then
- * add an `ExtendedZCollision` subclass around it to catch `TakeDamage()`
- * events that would normally target pipe bombs themselves. There we can add
- * additional checks to help us decide whether pipe bombs should actually
- * be damaged.
- *
- * `Timer()`. It would be simple to take control of this method by, say,
- * spamming `SetTimer(0.0, false)` every tick and then executing our own logic
- * elsewhere. However we only care about changin `Timer()`'s logic when pipe
- * bomb attempts to detect cnearby enemies and do not want to manage the rest
- * of `Timer()`'s logic.
- * Pipe bombs work like this: after being thrown they fall until they land,
- * invoking `Landed()` and `HitWall()` calls that start up the timer. Then
- * timer uses `ArmingCountDown` variable to count down the time for pipe to
- * "arm", that is, start functioning. It's after that that pipe starts to try
- * and detect nearby enemies until it decides to start exploding sequence,
- * instantly exploded with damage or disintegrated by siren's scream.
- * We want to catch the moment when `ArmingCountDown` reaches zero and
- * only then disable the timer. Then case we'll only need to reimplement logic
- * that takes care of enemy detection. We also do not need to constantly
- * re-reset the timer, since any code that will restart it would mean that
- * pipe bomb no longer needs to look for enemies.
- * When we detect enough enemies nearby or a change in pipe bomb's state
- * due to outside factors (being exploded or disintegrated) we simply need to
- * start it's the timer (if it was not done for us already).
- */
-
-// NOTE #1: setting either of `preventMassiveDamage` or
-// `preventSuspiciousDamage` might change how and where pipes might fall on
-// the ground (for example, pipe hats shoul not work anymore). In most cases,
-// however, there should be no difference from vanilla gameplay.
-// Setting this to `true` fixes a mechanic that allows pipes to deal extra
-// damage when detonated with a high fire rate weapons: pipes will now always
-// deal the same amount of damage.
-var public const config bool preventMassiveDamage;
-// It's possible for teammates to explode one's pipe under certain
-// circumstances. Setting this setting to `true` will prevent that
-// from happening.
-var public const config bool preventSuspiciousDamage;
-// Setting this to `true` will prevent pipe bombs from being detonated by
-// the nearby corpses on other player.
-var public const config bool preventCorpseDetonation;
-// Setting this to `true` will prevents pipe bombs from being detonated by
-// nearby KFO NPCs (Ringmaster Lockheart).
-var public const config bool preventNPCDetonation;
-// Certain spots prevent pipe bombs from having a line of sight to the
-// nearby zeds, preventing them from ever exploding. This issue can be resolves
-// by checking zed's proximity not to the pipe itself, but to a point directly
-// above it (20 units above by default).
-// If you wish to disable this part of the feature - set it to zero or
-// negative value. Otherwise it is suggested to keep it at `20`.
-var public const config float proximityCheckElevation;
-
-// Since this feature needs to do proximity check instead of pipes, we will
-// need to track all the pipes we will be doing checks for.
-// This struct contains all reference to the pipe itself and everything
-// else we need to track.
-struct PipeRecord
-{
- // Pipe that this record tracks.
- // Reference to `PipeBombProjectile`.
- var NativeActorRef pipe;
- // Each pipe has a separate timer for their scheduled proximity checks,
- // so we need a separate variable to track when that time comes for
- // each and every pipe
- var float timerCountDown;
- // `true` if we have already intercepted (and replaced with our own)
- // the proximity check for the pipe in this record
- var bool proximityCheckIntercepted;
- // Reference to the `ExtendedZCollision` we created to catch
- // `TakeDamage()` event.
- // Reference to `PipesSafetyCollision`.
- var NativeActorRef safetyCollision;
-};
-var private array pipeRecords;
-
-// Store the `bAlwaysRelevant` of the `class'PipeBombProjectile'` before this
-// feature activated to restore it in case it gets disabled
-// (we need to change `bAlwaysRelevant` in order to catch events of
-// pipe bombs spawning).
-var private bool pipesRelevancyFlag;
-
-var private Timer cleanupTimer;
+var public config bool preventMassiveDamage;
+var public config bool preventSuspiciousDamage;
+var public config bool preventCorpseDetonation;
+var public config bool preventNPCDetonation;
+var public config float proximityCheckElevation;
-protected function OnEnabled()
+protected function AssociativeArray ToData()
{
- local LevelInfo level;
- local PipeBombProjectile nextPipe;
- pipesRelevancyFlag = class'PipeBombProjectile'.default.bAlwaysRelevant;
- class'PipeBombProjectile'.default.bGameRelevant = false;
- // Set cleanup timer, there is little point to making
- // clean up interval configurable.
- cleanupTimer = _.time.StartTimer(5.0, true);
- cleanupTimer.OnElapsed(self).connect = CleanPipeRecords;
- // Fix pipes that are already lying about on the map
- level = _.unreal.GetLevel();
- foreach level.DynamicActors(class'KFMod.PipeBombProjectile', nextPipe) {
- RegisterPipe(nextPipe);
- }
-}
-
-protected function OnDisabled()
-{
- local int i;
- class'PipeBombProjectile'.default.bGameRelevant = pipesRelevancyFlag;
- cleanupTimer.FreeSelf();
- for (i = 0; i < pipeRecords.length; i += 1) {
- ReleasePipe(pipeRecords[i]);
- }
- pipeRecords.length = 0;
+ local AssociativeArray data;
+ data = __().collections.EmptyAssociativeArray();
+ data.SetBool(P("preventMassiveDamage"), preventMassiveDamage, true);
+ data.SetBool(P("preventSuspiciousDamage"), preventSuspiciousDamage, true);
+ data.SetBool(P("preventCorpseDetonation"), preventCorpseDetonation, true);
+ data.SetBool(P("preventNPCDetonation"), preventNPCDetonation, true);
+ data.SetFloat(P("proximityCheckElevation"), proximityCheckElevation, true);
+ return data;
}
-// Adds new pipe to our list and does necessary steps to replace logic of
-// `TakeDamage()` and `Timer()` methods.
-public final function RegisterPipe(PipeBombProjectile newPipe)
+protected function FromData(AssociativeArray source)
{
- local int i;
- local PipeRecord newRecord;
- if (newPipe == none) {
+ if (source == none) {
return;
}
- // Check whether we have already added this pipe
- for (i = 0; i < pipeRecords.length; i += 1)
- {
- if (pipeRecords[i].pipe.Get() == newPipe) {
- return;
- }
- }
- newRecord.pipe = _.unreal.ActorRef(newPipe);
- // Setup `PipesSafetyCollision` for catching `TakeDamage()` events
- // (only if we need to according to settings)
- if (NeedSafetyCollision())
- {
- newRecord.safetyCollision = _.unreal.ActorRef(
- class'PipesSafetyCollision'.static.ProtectPipes(newPipe));
- }
- pipeRecords[pipeRecords.length] = newRecord;
- // Intercept proximity checks (only if we need to according to settings)
- if (NeedManagedProximityChecks())
- {
- // We do this after we have added new pipe record to the complete list
- // so that we can redo the check early for
- // the previously recorded pipes
- InterceptProximityChecks();
- }
+ preventMassiveDamage = source.GetBool(P("preventMassiveDamage"));
+ preventSuspiciousDamage = source.GetBool(P("preventSuspiciousDamage"));
+ preventCorpseDetonation = source.GetBool(P("preventCorpseDetonation"));
+ preventNPCDetonation = source.GetBool(P("preventNPCDetonation"));
+ proximityCheckElevation = source.GetFloat(P("proximityCheckElevation"), 20);
}
-// Rolls back our changes to the pipe in the given `PipeRecord`.
-public final function ReleasePipe(PipeRecord pipeRecord)
+protected function DefaultIt()
{
- local PipeBombProjectile pipe;
- local PipesSafetyCollision safetyCollision;
- if (pipeRecord.safetyCollision != none)
- {
- safetyCollision =
- PipesSafetyCollision(pipeRecord.safetyCollision.Get());
- pipeRecord.safetyCollision.FreeSelf();
- pipeRecord.safetyCollision = none;
- }
- if (safetyCollision != none) {
- safetyCollision.TurnOff();
- }
- pipe = PipeBombProjectile(pipeRecord.pipe.Get());
- pipeRecord.pipe.FreeSelf();
- pipeRecord.pipe = none;
- if (pipeRecord.proximityCheckIntercepted && pipe != none)
- {
- pipeRecord.proximityCheckIntercepted = false;
- if (IsPipeDoingProximityChecks(pipe)) {
- pipe.SetTimer(pipeRecord.timerCountDown, true);
- }
- }
-}
-
-// Checks whether we actually need to use replace logic of `TakeDamage()`
-// method according to settings.
-private final function bool NeedSafetyCollision()
-{
- if (preventMassiveDamage) return true;
- if (preventSuspiciousDamage) return true;
- return false;
-}
-
-// Checks whether we actually need to use replace logic of `Timer()`
-// method according to settings.
-private final function bool NeedManagedProximityChecks()
-{
- if (preventCorpseDetonation) return true;
- if (preventNPCDetonation) return true;
- if (proximityCheckElevation > 0.0) return true;
- return false;
-}
-
-// Removes dead records with pipe instances turned into `none`
-private final function CleanPipeRecords(Timer source)
-{
- local int i;
- while (i < pipeRecords.length)
- {
- if (pipeRecords[i].pipe.Get() == none)
- {
- _.memory.Free(pipeRecords[i].pipe);
- _.memory.Free(pipeRecords[i].safetyCollision);
- pipeRecords.Remove(i, 1);
- }
- else {
- i += 1;
- }
- }
-}
-
-// Tries to replace logic of `SetTimer()` with our own for every pipe we
-// keep track of that:
-// 1. Started doing proximity checks;
-// 2. Have not yet had it's logic replaced.
-private final function InterceptProximityChecks()
-{
- local int i;
- local PipeBombProjectile nextPipe;
- for (i = 0; i < pipeRecords.length; i += 1)
- {
- if (pipeRecords[i].proximityCheckIntercepted) continue;
- nextPipe = PipeBombProjectile(pipeRecords[i].pipe.Get());
- if (nextPipe == none) continue;
-
- if (IsPipeDoingProximityChecks(nextPipe))
- {
- // Turn off pipe's own timer
- nextPipe.SetTimer(0, false);
- // We set `1.0` because that is the vanilla value;
- // Line 123 of "PipeBombProjectile.uc": `SetTimer(1.0,True);`
- pipeRecords[i].timerCountDown = 1.0;
- pipeRecords[i].proximityCheckIntercepted = true;
- }
- }
-}
-
-// Assumes `pipe != none`
-private final function bool IsPipeDoingProximityChecks(PipeBombProjectile pipe)
-{
- // These checks need to pass so that `Timer()` call from
- // "PipeBombProjectile.uc" enters the proximity check code
- // (starting at line 131)
- if (pipe.bHidden) return false;
- if (pipe.bTriggered) return false;
- if (pipe.bEnemyDetected) return false;
- return (pipe.armingCountDown < 0);
-}
-
-// Checks what pipes have their timers run out and doing proximity checks
-// for them
-private final function PerformProximityChecks(float delta)
-{
- local int i;
- local Vector checkLocation;
- local PipeBombProjectile nextPipe;
- for (i = 0; i < pipeRecords.length; i += 1)
- {
- pipeRecords[i].timerCountDown -= delta;
- if (pipeRecords[i].timerCountDown > 0) continue;
- nextPipe = PipeBombProjectile(pipeRecords[i].pipe.Get());
- if (nextPipe == none) continue;
- // `timerCountDown` does not makes sense for pipes that
- // are not doing proxiity checks
- if (!IsPipeDoingProximityChecks(nextPipe)) continue;
-
- checkLocation = nextPipe.location;
- if (proximityCheckElevation > 0) {
- checkLocation.z += proximityCheckElevation;
- }
- // This method repeats vanilla logic with some additional checks
- // and sets new timers by itself
- DoPipeProximityCheck(pipeRecords[i], checkLocation);
- }
-}
-
-// Original code is somewhat messy and was reworked in this more manageble
-// form as core logic is simple - for every nearby `Pawn` we increase the
-// percieved level for the pipe and when it reaches certain threshold
-// (`threatThreshhold = 1.0`) pipe explodes.
-// In native logic there are three cases for increasing threat level:
-// 1. Zeds increase it by a certain predefined
-// (in `KFMonster` class amount);
-// 2. Instigator (and his team, in case friendly fire is enabled and
-// they can be hurt by pipes) raise threat level by a negligible
-// amount (`0.001`) that will not lead to pipes exploding, but
-// will cause them to beep faster, warning about the danger;
-// 3. Some wacky code to check whether the `Pawn` is on the same team.
-// In a regualar killing floor game only something that is neither
-// player or zed can fit that. This is what causes corpses and
-// KFO NPCs to explode pipes.
-// Threat level is not directly set in vanilla code for
-// this case, instead performing two operations
-// `bEnemyDetected=true;` and `SetTimer(0.15,True);`.
-// However, given the code that follows these calls, executing them
-// is equivalent to adding `threatThreshhold` to the current
-// threat level. Which is what we do here instead.
-private final function DoPipeProximityCheck(
- out PipeRecord pipeRecord,
- Vector checkLocation)
-{
- local Pawn checkPawn;
- local float threatLevel;
- local PipeBombProjectile pipe;
- pipe = PipeBombProjectile(pipeRecord.pipe.Get());
- if (pipe == none) {
- return;
- }
- pipe.bAlwaysRelevant = false;
- pipe.PlaySound(pipe.beepSound,, 0.5,, 50.0);
- // Out rewritten logic, which should do exactly the same:
- foreach pipe.VisibleCollidingActors( class'Pawn', checkPawn,
- pipe.detectionRadius, checkLocation)
- {
- threatLevel += GetThreatLevel(pipe, checkPawn);
- // Explosion! No need to bother with the rest of the `Pawn`s.
- if (threatLevel >= pipe.threatThreshhold) {
- break;
- }
- }
- // Resume vanilla code from "PipeBombProjectile.uc", lines from 169 to 181:
- if(threatLevel >= pipe.threatThreshhold)
- {
- pipe.bEnemyDetected = true;
- pipe.SetTimer(0.15, true);
- }
- else if(threatLevel > 0) {
- pipeRecord.timerCountDown = 0.5;
- }
- else {
- pipeRecord.timerCountDown = 1.0;
- }
-}
-
-// Threat level calculations are moves to a separate method to make algorithm
-// less cumbersome
-private final function float GetThreatLevel(
- PipeBombProjectile pipe,
- Pawn checkPawn)
-{
- local bool onSameTeam;
- local bool friendlyFireEnabled;
- local KFGameType kfGame;
- local PlayerReplicationInfo playerRI;
- local KFMonster zed;
- if (pipe == none) return 0.0;
- if (checkPawn == none) return 0.0;
-
- playerRI = checkPawn.playerReplicationInfo;
- if (pipe.level != none) {
- kfGame = KFGameType(pipe.level.game);
- }
- if (kfGame != none) {
- friendlyFireEnabled = kfGame.friendlyFireScale > 0;
- }
- // Warn teammates about pipes
- onSameTeam = playerRI != none && playerRI.team.teamIndex == pipe.placedTeam;
- if (checkPawn == pipe.instigator || (friendlyFireEnabled && onSameTeam)) {
- return 0.001;
- }
- // Count zed's threat score
- zed = KFMonster(checkPawn);
- if (zed != none) {
- return zed.motionDetectorThreat;
- }
- // Managed checks
- if (preventCorpseDetonation && checkPawn.health <= 0) {
- return 0.0;
- }
- if (preventNPCDetonation && KF_StoryNPC(CheckPawn) != none) {
- return 0.0;
- }
- // Something weird demands a special full-alert treatment.
- // Some checks are removed:
- // 1. Removed `checkPawn.Role == ROLE_Authority` check, since we are
- // working on server exclusively;
- // 2. Removed `checkPawn != instigator` since, if
- // `checkPawn == pipe.instigator`, previous `if` block will
- // prevent us from reaching this point
- if( playerRI != none && playerRI.team.teamIndex != pipe.placedTeam
- || checkPawn.GetTeamNum() != pipe.placedTeam)
- {
- // Full threat score
- return pipe.threatThreshhold;
- }
- return 0.0;
-}
-
-event Tick(float delta)
-{
- if (NeedManagedProximityChecks())
- {
- InterceptProximityChecks();
- PerformProximityChecks(delta);
- }
+ preventMassiveDamage = true;
+ preventSuspiciousDamage = true;
+ preventCorpseDetonation = true;
+ preventNPCDetonation = true;
+ proximityCheckElevation = 20.0;
}
defaultproperties
{
- preventMassiveDamage = true
- preventSuspiciousDamage = true
- preventCorpseDetonation = true
- preventNPCDetonation = true
- proximityCheckElevation = 20.0
- // Listeners
- requiredListeners(0) = class'MutatorListener_FixPipes'
+ configName = "AcediaFixes"
+ preventMassiveDamage = true
+ preventSuspiciousDamage = true
+ preventCorpseDetonation = true
+ preventNPCDetonation = true
+ proximityCheckElevation = 20.0
}
\ No newline at end of file
diff --git a/sources/FixPipes/FixPipes_Feature.uc b/sources/FixPipes/FixPipes_Feature.uc
new file mode 100644
index 0000000..2a3212a
--- /dev/null
+++ b/sources/FixPipes/FixPipes_Feature.uc
@@ -0,0 +1,480 @@
+/**
+ * This feature addresses several bugs related to pipe bombs:
+ * 1. Gaining extra explosive damage by shooting pipes with
+ * a high fire rate weapon;
+ * 2. Other players exploding one's pipes;
+ * 3. Corpses and story NPCs exploding nearby pipes;
+ * 4. Pipes being stuck in places where they cannot detect nearby
+ * enemies and, therefore, not exploding.
+ * Copyright 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 .
+ */
+class FixPipes_Feature extends Feature;
+
+/**
+ * There are two main culprits for the issues we are interested in:
+ * `TakeDamage()` and `Timer()` methods. `TakeDamage()` lacks sufficient checks
+ * for instigator allows for ways to damage it, causing explosion. It also
+ * lacks any checks for whether pipe bom already exploded, allowing players to
+ * damage it several time before it's destroyed, causing explosion every time.
+ * `Timer()` method simply contains a porximity check for hostile pawns that
+ * wrongfully detects certain actor (like `KF_StoryNPC` or players' corpses)
+ * and cannot detect zeds around itself if pipe is placed in a way that
+ * prevents it from having a direct line of sight towards zeds.
+ * To fix our issues we have to somehow overload both of these methods,
+ * which is impossible while keeping Acedia server-side. So we will have to
+ * do some hacks.
+ *
+ * `TakeDamage()`. To override this method's behavior we will remove actor
+ * collision from pipe bombs, preventing it from being directly damaged, then
+ * add an `ExtendedZCollision` subclass around it to catch `TakeDamage()`
+ * events that would normally target pipe bombs themselves. There we can add
+ * additional checks to help us decide whether pipe bombs should actually
+ * be damaged.
+ *
+ * `Timer()`. It would be simple to take control of this method by, say,
+ * spamming `SetTimer(0.0, false)` every tick and then executing our own logic
+ * elsewhere. However we only care about changin `Timer()`'s logic when pipe
+ * bomb attempts to detect cnearby enemies and do not want to manage the rest
+ * of `Timer()`'s logic.
+ * Pipe bombs work like this: after being thrown they fall until they land,
+ * invoking `Landed()` and `HitWall()` calls that start up the timer. Then
+ * timer uses `ArmingCountDown` variable to count down the time for pipe to
+ * "arm", that is, start functioning. It's after that that pipe starts to try
+ * and detect nearby enemies until it decides to start exploding sequence,
+ * instantly exploded with damage or disintegrated by siren's scream.
+ * We want to catch the moment when `ArmingCountDown` reaches zero and
+ * only then disable the timer. Then case we'll only need to reimplement logic
+ * that takes care of enemy detection. We also do not need to constantly
+ * re-reset the timer, since any code that will restart it would mean that
+ * pipe bomb no longer needs to look for enemies.
+ * When we detect enough enemies nearby or a change in pipe bomb's state
+ * due to outside factors (being exploded or disintegrated) we simply need to
+ * start it's the timer (if it was not done for us already).
+ */
+
+// NOTE #1: setting either of `preventMassiveDamage` or
+// `preventSuspiciousDamage` might change how and where pipes might fall on
+// the ground (for example, pipe hats shoul not work anymore). In most cases,
+// however, there should be no difference from vanilla gameplay.
+// Setting this to `true` fixes a mechanic that allows pipes to deal extra
+// damage when detonated with a high fire rate weapons: pipes will now always
+// deal the same amount of damage.
+var public /*config*/ bool preventMassiveDamage;
+// It's possible for teammates to explode one's pipe under certain
+// circumstances. Setting this setting to `true` will prevent that
+// from happening.
+var public /*config*/ bool preventSuspiciousDamage;
+// Setting this to `true` will prevent pipe bombs from being detonated by
+// the nearby corpses on other player.
+var public /*config*/ bool preventCorpseDetonation;
+// Setting this to `true` will prevents pipe bombs from being detonated by
+// nearby KFO NPCs (Ringmaster Lockheart).
+var public /*config*/ bool preventNPCDetonation;
+// Certain spots prevent pipe bombs from having a line of sight to the
+// nearby zeds, preventing them from ever exploding. This issue can be resolves
+// by checking zed's proximity not to the pipe itself, but to a point directly
+// above it (20 units above by default).
+// If you wish to disable this part of the feature - set it to zero or
+// negative value. Otherwise it is suggested to keep it at `20`.
+var public /*config*/ float proximityCheckElevation;
+
+// Since this feature needs to do proximity check instead of pipes, we will
+// need to track all the pipes we will be doing checks for.
+// This struct contains all reference to the pipe itself and everything
+// else we need to track.
+struct PipeRecord
+{
+ // Pipe that this record tracks.
+ // Reference to `PipeBombProjectile`.
+ var NativeActorRef pipe;
+ // Each pipe has a separate timer for their scheduled proximity checks,
+ // so we need a separate variable to track when that time comes for
+ // each and every pipe
+ var float timerCountDown;
+ // `true` if we have already intercepted (and replaced with our own)
+ // the proximity check for the pipe in this record
+ var bool proximityCheckIntercepted;
+ // Reference to the `ExtendedZCollision` we created to catch
+ // `TakeDamage()` event.
+ // Reference to `PipesSafetyCollision`.
+ var NativeActorRef safetyCollision;
+};
+var private array pipeRecords;
+
+// Store the `bAlwaysRelevant` of the `class'PipeBombProjectile'` before this
+// feature activated to restore it in case it gets disabled
+// (we need to change `bAlwaysRelevant` in order to catch events of
+// pipe bombs spawning).
+var private bool pipesRelevancyFlag;
+
+var private Timer cleanupTimer;
+
+protected function OnEnabled()
+{
+ local LevelInfo level;
+ local PipeBombProjectile nextPipe;
+ pipesRelevancyFlag = class'PipeBombProjectile'.default.bAlwaysRelevant;
+ class'PipeBombProjectile'.default.bGameRelevant = false;
+ _.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement;
+ // Set cleanup timer, there is little point to making
+ // clean up interval configurable.
+ cleanupTimer = _.time.StartTimer(5.0, true);
+ cleanupTimer.OnElapsed(self).connect = CleanPipeRecords;
+ // Fix pipes that are already lying about on the map
+ level = _.unreal.GetLevel();
+ foreach level.DynamicActors(class'KFMod.PipeBombProjectile', nextPipe) {
+ RegisterPipe(nextPipe);
+ }
+}
+
+protected function OnDisabled()
+{
+ local int i;
+ class'PipeBombProjectile'.default.bGameRelevant = pipesRelevancyFlag;
+ _.unreal.mutator.OnCheckReplacement(self).Disconnect();
+ cleanupTimer.FreeSelf();
+ for (i = 0; i < pipeRecords.length; i += 1) {
+ ReleasePipe(pipeRecords[i]);
+ }
+ pipeRecords.length = 0;
+}
+
+protected function SwapConfig(FeatureConfig config)
+{
+ local int i;
+ local FixPipes newConfig;
+ newConfig = FixPipes(config);
+ if (newConfig == none) {
+ return;
+ }
+ preventMassiveDamage = newConfig.preventMassiveDamage;
+ preventSuspiciousDamage = newConfig.preventSuspiciousDamage;
+ preventCorpseDetonation = newConfig.preventCorpseDetonation;
+ preventNPCDetonation = newConfig.preventNPCDetonation;
+ proximityCheckElevation = newConfig.proximityCheckElevation;
+ for (i = 0; i < pipeRecords.length; i += 1)
+ {
+ ReleasePipe(pipeRecords[i]);
+ RegisterPipe(PipeBombProjectile(pipeRecords[i].safetyCollision.Get()));
+ }
+}
+
+private function bool CheckReplacement(Actor other, out byte isSuperRelevant)
+{
+ local PipeBombProjectile pipeProjectile;
+ pipeProjectile = PipeBombProjectile(other);
+ if (pipeProjectile != none) {
+ RegisterPipe(PipeBombProjectile(other));
+ }
+ return true;
+}
+
+// Adds new pipe to our list and does necessary steps to replace logic of
+// `TakeDamage()` and `Timer()` methods.
+public final function RegisterPipe(PipeBombProjectile newPipe)
+{
+ local int i;
+ local PipeRecord newRecord;
+ if (newPipe == none) {
+ return;
+ }
+ // Check whether we have already added this pipe
+ for (i = 0; i < pipeRecords.length; i += 1)
+ {
+ if (pipeRecords[i].pipe.Get() == newPipe) {
+ return;
+ }
+ }
+ newRecord.pipe = _.unreal.ActorRef(newPipe);
+ // Setup `PipesSafetyCollision` for catching `TakeDamage()` events
+ // (only if we need to according to settings)
+ if (NeedSafetyCollision())
+ {
+ newRecord.safetyCollision = _.unreal.ActorRef(
+ class'PipesSafetyCollision'.static.ProtectPipes(newPipe));
+ }
+ pipeRecords[pipeRecords.length] = newRecord;
+ // Intercept proximity checks (only if we need to according to settings)
+ if (NeedManagedProximityChecks())
+ {
+ // We do this after we have added new pipe record to the complete list
+ // so that we can redo the check early for
+ // the previously recorded pipes
+ InterceptProximityChecks();
+ }
+}
+
+// Rolls back our changes to the pipe in the given `PipeRecord`.
+public final function ReleasePipe(PipeRecord pipeRecord)
+{
+ local PipeBombProjectile pipe;
+ local PipesSafetyCollision safetyCollision;
+ if (pipeRecord.safetyCollision != none)
+ {
+ safetyCollision =
+ PipesSafetyCollision(pipeRecord.safetyCollision.Get());
+ pipeRecord.safetyCollision.FreeSelf();
+ pipeRecord.safetyCollision = none;
+ }
+ if (safetyCollision != none) {
+ safetyCollision.TurnOff();
+ }
+ pipe = PipeBombProjectile(pipeRecord.pipe.Get());
+ pipeRecord.pipe.FreeSelf();
+ pipeRecord.pipe = none;
+ if (pipeRecord.proximityCheckIntercepted && pipe != none)
+ {
+ pipeRecord.proximityCheckIntercepted = false;
+ if (IsPipeDoingProximityChecks(pipe)) {
+ pipe.SetTimer(pipeRecord.timerCountDown, true);
+ }
+ }
+}
+
+// Checks whether we actually need to use replace logic of `TakeDamage()`
+// method according to settings.
+private final function bool NeedSafetyCollision()
+{
+ if (preventMassiveDamage) return true;
+ if (preventSuspiciousDamage) return true;
+ return false;
+}
+
+// Checks whether we actually need to use replace logic of `Timer()`
+// method according to settings.
+private final function bool NeedManagedProximityChecks()
+{
+ if (preventCorpseDetonation) return true;
+ if (preventNPCDetonation) return true;
+ if (proximityCheckElevation > 0.0) return true;
+ return false;
+}
+
+// Removes dead records with pipe instances turned into `none`
+private final function CleanPipeRecords(Timer source)
+{
+ local int i;
+ while (i < pipeRecords.length)
+ {
+ if (pipeRecords[i].pipe.Get() == none)
+ {
+ _.memory.Free(pipeRecords[i].pipe);
+ _.memory.Free(pipeRecords[i].safetyCollision);
+ pipeRecords.Remove(i, 1);
+ }
+ else {
+ i += 1;
+ }
+ }
+}
+
+// Tries to replace logic of `SetTimer()` with our own for every pipe we
+// keep track of that:
+// 1. Started doing proximity checks;
+// 2. Have not yet had it's logic replaced.
+private final function InterceptProximityChecks()
+{
+ local int i;
+ local PipeBombProjectile nextPipe;
+ for (i = 0; i < pipeRecords.length; i += 1)
+ {
+ if (pipeRecords[i].proximityCheckIntercepted) continue;
+ nextPipe = PipeBombProjectile(pipeRecords[i].pipe.Get());
+ if (nextPipe == none) continue;
+
+ if (IsPipeDoingProximityChecks(nextPipe))
+ {
+ // Turn off pipe's own timer
+ nextPipe.SetTimer(0, false);
+ // We set `1.0` because that is the vanilla value;
+ // Line 123 of "PipeBombProjectile.uc": `SetTimer(1.0,True);`
+ pipeRecords[i].timerCountDown = 1.0;
+ pipeRecords[i].proximityCheckIntercepted = true;
+ }
+ }
+}
+
+// Assumes `pipe != none`
+private final function bool IsPipeDoingProximityChecks(PipeBombProjectile pipe)
+{
+ // These checks need to pass so that `Timer()` call from
+ // "PipeBombProjectile.uc" enters the proximity check code
+ // (starting at line 131)
+ if (pipe.bHidden) return false;
+ if (pipe.bTriggered) return false;
+ if (pipe.bEnemyDetected) return false;
+ return (pipe.armingCountDown < 0);
+}
+
+// Checks what pipes have their timers run out and doing proximity checks
+// for them
+private final function PerformProximityChecks(float delta)
+{
+ local int i;
+ local Vector checkLocation;
+ local PipeBombProjectile nextPipe;
+ for (i = 0; i < pipeRecords.length; i += 1)
+ {
+ pipeRecords[i].timerCountDown -= delta;
+ if (pipeRecords[i].timerCountDown > 0) continue;
+ nextPipe = PipeBombProjectile(pipeRecords[i].pipe.Get());
+ if (nextPipe == none) continue;
+ // `timerCountDown` does not makes sense for pipes that
+ // are not doing proxiity checks
+ if (!IsPipeDoingProximityChecks(nextPipe)) continue;
+
+ checkLocation = nextPipe.location;
+ if (proximityCheckElevation > 0) {
+ checkLocation.z += proximityCheckElevation;
+ }
+ // This method repeats vanilla logic with some additional checks
+ // and sets new timers by itself
+ DoPipeProximityCheck(pipeRecords[i], checkLocation);
+ }
+}
+
+// Original code is somewhat messy and was reworked in this more manageble
+// form as core logic is simple - for every nearby `Pawn` we increase the
+// percieved level for the pipe and when it reaches certain threshold
+// (`threatThreshhold = 1.0`) pipe explodes.
+// In native logic there are three cases for increasing threat level:
+// 1. Zeds increase it by a certain predefined
+// (in `KFMonster` class amount);
+// 2. Instigator (and his team, in case friendly fire is enabled and
+// they can be hurt by pipes) raise threat level by a negligible
+// amount (`0.001`) that will not lead to pipes exploding, but
+// will cause them to beep faster, warning about the danger;
+// 3. Some wacky code to check whether the `Pawn` is on the same team.
+// In a regualar killing floor game only something that is neither
+// player or zed can fit that. This is what causes corpses and
+// KFO NPCs to explode pipes.
+// Threat level is not directly set in vanilla code for
+// this case, instead performing two operations
+// `bEnemyDetected=true;` and `SetTimer(0.15,True);`.
+// However, given the code that follows these calls, executing them
+// is equivalent to adding `threatThreshhold` to the current
+// threat level. Which is what we do here instead.
+private final function DoPipeProximityCheck(
+ out PipeRecord pipeRecord,
+ Vector checkLocation)
+{
+ local Pawn checkPawn;
+ local float threatLevel;
+ local PipeBombProjectile pipe;
+ pipe = PipeBombProjectile(pipeRecord.pipe.Get());
+ if (pipe == none) {
+ return;
+ }
+ pipe.bAlwaysRelevant = false;
+ pipe.PlaySound(pipe.beepSound,, 0.5,, 50.0);
+ // Out rewritten logic, which should do exactly the same:
+ foreach pipe.VisibleCollidingActors( class'Pawn', checkPawn,
+ pipe.detectionRadius, checkLocation)
+ {
+ threatLevel += GetThreatLevel(pipe, checkPawn);
+ // Explosion! No need to bother with the rest of the `Pawn`s.
+ if (threatLevel >= pipe.threatThreshhold) {
+ break;
+ }
+ }
+ // Resume vanilla code from "PipeBombProjectile.uc", lines from 169 to 181:
+ if(threatLevel >= pipe.threatThreshhold)
+ {
+ pipe.bEnemyDetected = true;
+ pipe.SetTimer(0.15, true);
+ }
+ else if(threatLevel > 0) {
+ pipeRecord.timerCountDown = 0.5;
+ }
+ else {
+ pipeRecord.timerCountDown = 1.0;
+ }
+}
+
+// Threat level calculations are moves to a separate method to make algorithm
+// less cumbersome
+private final function float GetThreatLevel(
+ PipeBombProjectile pipe,
+ Pawn checkPawn)
+{
+ local bool onSameTeam;
+ local bool friendlyFireEnabled;
+ local KFGameType kfGame;
+ local PlayerReplicationInfo playerRI;
+ local KFMonster zed;
+ if (pipe == none) return 0.0;
+ if (checkPawn == none) return 0.0;
+
+ playerRI = checkPawn.playerReplicationInfo;
+ if (pipe.level != none) {
+ kfGame = KFGameType(pipe.level.game);
+ }
+ if (kfGame != none) {
+ friendlyFireEnabled = kfGame.friendlyFireScale > 0;
+ }
+ // Warn teammates about pipes
+ onSameTeam = playerRI != none && playerRI.team.teamIndex == pipe.placedTeam;
+ if (checkPawn == pipe.instigator || (friendlyFireEnabled && onSameTeam)) {
+ return 0.001;
+ }
+ // Count zed's threat score
+ zed = KFMonster(checkPawn);
+ if (zed != none) {
+ return zed.motionDetectorThreat;
+ }
+ // Managed checks
+ if (preventCorpseDetonation && checkPawn.health <= 0) {
+ return 0.0;
+ }
+ if (preventNPCDetonation && KF_StoryNPC(CheckPawn) != none) {
+ return 0.0;
+ }
+ // Something weird demands a special full-alert treatment.
+ // Some checks are removed:
+ // 1. Removed `checkPawn.Role == ROLE_Authority` check, since we are
+ // working on server exclusively;
+ // 2. Removed `checkPawn != instigator` since, if
+ // `checkPawn == pipe.instigator`, previous `if` block will
+ // prevent us from reaching this point
+ if( playerRI != none && playerRI.team.teamIndex != pipe.placedTeam
+ || checkPawn.GetTeamNum() != pipe.placedTeam)
+ {
+ // Full threat score
+ return pipe.threatThreshhold;
+ }
+ return 0.0;
+}
+
+event Tick(float delta)
+{
+ if (NeedManagedProximityChecks())
+ {
+ InterceptProximityChecks();
+ PerformProximityChecks(delta);
+ }
+}
+
+defaultproperties
+{
+ configClass = class'FixPipes'
+ preventMassiveDamage = true
+ preventSuspiciousDamage = true
+ preventCorpseDetonation = true
+ preventNPCDetonation = true
+ proximityCheckElevation = 20.0
+}
\ No newline at end of file
diff --git a/sources/FixPipes/MutatorListener_FixPipes.uc b/sources/FixPipes/MutatorListener_FixPipes.uc
deleted file mode 100644
index 92a880a..0000000
--- a/sources/FixPipes/MutatorListener_FixPipes.uc
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Overloaded mutator events listener to register spawned pipe bombs.
- * Copyright 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 .
- */
-class MutatorListener_FixPipes extends MutatorListenerBase
- abstract;
-
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- local FixPipes pipesFix;
- local PipeBombProjectile pipeProjectile;
- pipeProjectile = PipeBombProjectile(other);
- if (pipeProjectile == none) return true;
- pipesFix = FixPipes(class'FixPipes'.static.GetInstance());
- if (pipesFix == none) return true;
-
- pipesFix.RegisterPipe(pipeProjectile);
- return true;
-}
-
-defaultproperties
-{
- relatedEvents = class'MutatorEvents'
-}
\ No newline at end of file
diff --git a/sources/FixPipes/PipesSafetyCollision.uc b/sources/FixPipes/PipesSafetyCollision.uc
index 73d6442..7f7f0d2 100644
--- a/sources/FixPipes/PipesSafetyCollision.uc
+++ b/sources/FixPipes/PipesSafetyCollision.uc
@@ -47,8 +47,9 @@ public final static function PipesSafetyCollision ProtectPipes(
private function bool IsSuspicious(Pawn instigator)
{
// Instigator vanished
- if (instigator == none) return true;
-
+ if (instigator == none) {
+ return true;
+ }
// Instigator already became spectator
if (KFPawn(instigator) != none)
{
@@ -80,11 +81,11 @@ function TakeDamage(
class damageType,
optional int hitIndex)
{
- local FixPipes pipesFix;
+ local FixPipes_Feature pipesFix;
local PipeBombProjectile target;
target = PipeBombProjectile(owner);
if (target == none) return;
- pipesFix = FixPipes(class'FixPipes'.static.GetInstance());
+ pipesFix = FixPipes_Feature(class'FixPipes_Feature'.static.GetInstance());
if (pipesFix == none) return;
if (pipesFix.preventMassiveDamage && target.bTriggered) return;
if (pipesFix.preventSuspiciousDamage && IsSuspicious(instigator)) return;
diff --git a/sources/FixProjectileFF/FixProjectileFF.uc b/sources/FixProjectileFF/FixProjectileFF.uc
index 3a2c277..b66f78e 100644
--- a/sources/FixProjectileFF/FixProjectileFF.uc
+++ b/sources/FixProjectileFF/FixProjectileFF.uc
@@ -1,12 +1,5 @@
/**
- * This feature addresses the bug that allows teammates to explode some of
- * the player's projectiles by damaging them even when friendly fire is
- * turned off, therefore killing the player (whether by accident or not).
- *
- * Problem is solved by "disarming" projectiles vulnerable to this
- * friendly fire and replacing them with our own class of projectile that is
- * spawned only on a server and does additional safety checks to ensure it will
- * only explode when it is expected from it.
+ * Config object for `FixProjectileFF_Feature`.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
@@ -24,138 +17,34 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class FixProjectileFF extends Feature
+class FixProjectileFF extends FeatureConfig
+ perobjectconfig
config(AcediaFixes);
-/**
- * All projectiles vulnerable to this bug (we consider only those that can
- * explode and harm the player) are derived from `ROBallisticProjectile`. When
- * one of them is spawned we will:
- * 1. Set it's damage parameters to zero, to ensure that it won't damage
- * anyone or anything if other measures fail;
- * 2. Disable it's collision, preventing it from being accessed via
- * `VisibleCollidingActors()` method that is usually used to damage
- * these projectiles.
- * Then we spawn our own version of the projectile with fixed
- * `TakeDamage()` that does additional check that either it's projectile's
- * owner that damages it or friendly fire is enabled.
- * To do this replacement we will need to catch the moment when vanilla
- * projectile spawns. Unfortunately, this cannot be done by default, since all
- * projectiles have `bGameRelevant` flag set to `true`, which prevents
- * `CheckReplacement()` from being called for them. So, when feature is
- * enabled, it forces all the projectiles we are interested in to have
- * `bGameRelevant = false`.
- *
- * Issue arises from the fact that these two projectiles can desynchronize:
- * old projectile, that client sees, might not be in the same location as the
- * new one (especially since we are disabling collisions for the old one), that
- * deals damage. There are two cases to consider, depending on
- * the `bNetTemporary` of the old projectile:
- * 1. `bNetTemporary == true`: projectile version on client side is
- * independent from the server-side's one. In this case there is
- * nothing we can do. In fact, vanilla game suffers from this very bug
- * that makes, say, M79 projectile fly past the zed it exploded
- * (usually after killing it). To deal with this we would need
- * the ability to affect client's code, which cannot be done in
- * the server mode.
- * 2. `bNetTemporary == true`: projectile version on client side is
- * actually synchronized with the server-side's one. In this case we
- * will simply make new projectile to constantly force the old one to
- * be in the same place at the same rotation. We will also propagate
- * various state-changing events such as exploding, disintegrating from
- * siren's scream or sticking to the wall/zed. That is, we will make
- * the old projectile (that client can see) "the face" of the new one
- * (that client cannot see). Only "The Orca Bomb Propeller" and
- * "SealSqueal Harpoon Bomber" fall into this group.
- */
-
-// By default (`ignoreFriendlyFire == false`), when friendly fire is enabled
-// (at any level), this fix allows other players to explode one's projectiles.
-// By setting this to `true` you will force this fix to prevent it no matter
-// what, even when server has full friendly fire enabled.
-var private config bool ignoreFriendlyFire;
-
-// Stores what pairs of projectile classes that describe what (vulnerable)
-// class must be replaced with what (protected class). It also remembers the
-// previous state of `bGameRelevant` for replaceable class to restore it in
-// case this feature is disabled.
-struct ReplacementRule
-{
- // `bGameRelevant` before this feature set it to `false`
- var bool relevancyFlag;
- var class vulnerableClass;
- var class protectedClass;
-};
-var private const array rules;
-
-protected function OnEnabled()
-{
- local int i;
- for (i = 0; i < rules.length; i += 1)
- {
- if (rules[i].vulnerableClass == none) continue;
- if (rules[i].protectedClass == none) continue;
- rules[i].relevancyFlag = rules[i].vulnerableClass.default.bGameRelevant;
- rules[i].vulnerableClass.default.bGameRelevant = false;
- }
-}
+var public config bool ignoreFriendlyFire;
-protected function OnDisabled()
+protected function AssociativeArray ToData()
{
- local int i;
- for (i = 0; i < rules.length; i += 1)
- {
- if (rules[i].vulnerableClass == none) continue;
- if (rules[i].protectedClass == none) continue;
- rules[i].vulnerableClass.default.bGameRelevant = rules[i].relevancyFlag;
- }
+ local AssociativeArray data;
+ data = __().collections.EmptyAssociativeArray();
+ data.SetBool(P("ignoreFriendlyFire"), ignoreFriendlyFire, true);
+ return data;
}
-// Returns "fixed" class that no longer explodes from random damage
-public final static function class FindFixedClass(
- class projectileClass)
+protected function FromData(AssociativeArray source)
{
- local int i;
- local array rulesCopy;
- if (projectileClass == none) return none;
-
- rulesCopy = default.rules;
- for (i = 0; i < rulesCopy.length; i += 1)
- {
- if (rulesCopy[i].vulnerableClass == projectileClass) {
- return rulesCopy[i].protectedClass;
- }
+ if (source != none) {
+ ignoreFriendlyFire = source.GetBool(P("ignoreFriendlyFire"));
}
- return projectileClass;
}
-// Check if, according to this fix, projectiles should explode from
-// friendly fire
-// If `FixProjectileFF` in disabled always returns `false`.
-public final static function bool IsFriendlyFireAcceptable()
+protected function DefaultIt()
{
- local FixProjectileFF projectileFFFix;
- projectileFFFix = FixProjectileFF(GetInstance());
- if (projectileFFFix == none) return false;
- if (projectileFFFix.ignoreFriendlyFire) return false;
-
- return __().unreal.GetKFGameType().friendlyFireScale > 0;
+ ignoreFriendlyFire = false;
}
defaultproperties
{
+ configName = "AcediaFixes"
ignoreFriendlyFire = false
- rules(0) = (vulnerableClass=class'KFMod.M79GrenadeProjectile',protectedClass=class'FixProjectileFFClass_M79GrenadeProjectile')
- rules(1) = (vulnerableClass=class'KFMod.M32GrenadeProjectile',protectedClass=class'FixProjectileFFClass_M32GrenadeProjectile')
- rules(2) = (vulnerableClass=class'KFMod.LAWProj',protectedClass=class'FixProjectileFFClass_LAWProj')
- rules(3) = (vulnerableClass=class'KFMod.M203GrenadeProjectile',protectedClass=class'FixProjectileFFClass_M203GrenadeProjectile')
- rules(4) = (vulnerableClass=class'KFMod.ZEDGunProjectile',protectedClass=class'FixProjectileFFClass_ZEDGunProjectile')
- rules(5) = (vulnerableClass=class'KFMod.ZEDMKIIPrimaryProjectile',protectedClass=class'FixProjectileFFClass_ZEDMKIIPrimaryProjectile')
- rules(6) = (vulnerableClass=class'KFMod.ZEDMKIISecondaryProjectile',protectedClass=class'FixProjectileFFClass_ZEDMKIISecondaryProjectile')
- rules(7) = (vulnerableClass=class'KFMod.FlareRevolverProjectile',protectedClass=class'FixProjectileFFClass_FlareRevolverProjectile')
- rules(8) = (vulnerableClass=class'KFMod.HuskGunProjectile',protectedClass=class'FixProjectileFFClass_HuskGunProjectile')
- rules(9) = (vulnerableClass=class'KFMod.SPGrenadeProjectile',protectedClass=class'FixProjectileFFClass_SPGrenadeProjectile')
- rules(10) = (vulnerableClass=class'KFMod.SealSquealProjectile',protectedClass=class'FixProjectileFFClass_SealSquealProjectile')
- // Listeners
- requiredListeners(0) = class'MutatorListener_FixProjectileFF'
}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixProjectileFF_Feature.uc b/sources/FixProjectileFF/FixProjectileFF_Feature.uc
new file mode 100644
index 0000000..b9bf80f
--- /dev/null
+++ b/sources/FixProjectileFF/FixProjectileFF_Feature.uc
@@ -0,0 +1,312 @@
+/**
+ * This feature addresses the bug that allows teammates to explode some of
+ * the player's projectiles by damaging them even when friendly fire is
+ * turned off, therefore killing the player (whether by accident or not).
+ *
+ * Problem is solved by "disarming" projectiles vulnerable to this
+ * friendly fire and replacing them with our own class of projectile that is
+ * spawned only on a server and does additional safety checks to ensure it will
+ * only explode when it is expected from it.
+ * Copyright 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 .
+ */
+class FixProjectileFF_Feature extends Feature;
+
+/**
+ * All projectiles vulnerable to this bug (we consider only those that can
+ * explode and harm the player) are derived from `ROBallisticProjectile`. When
+ * one of them is spawned we will:
+ * 1. Set it's damage parameters to zero, to ensure that it won't damage
+ * anyone or anything if other measures fail;
+ * 2. Disable it's collision, preventing it from being accessed via
+ * `VisibleCollidingActors()` method that is usually used to damage
+ * these projectiles.
+ * Then we spawn our own version of the projectile with fixed
+ * `TakeDamage()` that does additional check that either it's projectile's
+ * owner that damages it or friendly fire is enabled.
+ * To do this replacement we will need to catch the moment when vanilla
+ * projectile spawns. Unfortunately, this cannot be done by default, since all
+ * projectiles have `bGameRelevant` flag set to `true`, which prevents
+ * `CheckReplacement()` from being called for them. So, when feature is
+ * enabled, it forces all the projectiles we are interested in to have
+ * `bGameRelevant = false`.
+ *
+ * Issue arises from the fact that these two projectiles can desynchronize:
+ * old projectile, that client sees, might not be in the same location as the
+ * new one (especially since we are disabling collisions for the old one), that
+ * deals damage. There are two cases to consider, depending on
+ * the `bNetTemporary` of the old projectile:
+ * 1. `bNetTemporary == true`: projectile version on client side is
+ * independent from the server-side's one. In this case there is
+ * nothing we can do. In fact, vanilla game suffers from this very bug
+ * that makes, say, M79 projectile fly past the zed it exploded
+ * (usually after killing it). To deal with this we would need
+ * the ability to affect client's code, which cannot be done in
+ * the server mode.
+ * 2. `bNetTemporary == false`: projectile version on client side is
+ * actually synchronized with the server-side's one. In this case we
+ * will simply make new projectile to constantly force the old one to
+ * be in the same place at the same rotation. We will also propagate
+ * various state-changing events such as exploding, disintegrating from
+ * siren's scream or sticking to the wall/zed. That is, we will make
+ * the old projectile (that client can see) "the face" of the new one
+ * (that client cannot see). Only "The Orca Bomb Propeller" and
+ * "SealSqueal Harpoon Bomber" fall into this group.
+ */
+
+// By default (`ignoreFriendlyFire == false`), when friendly fire is enabled
+// (at any level), this fix allows other players to explode one's projectiles.
+// By setting this to `true` you will force this fix to prevent it no matter
+// what, even when server has full friendly fire enabled.
+var private /*config*/ bool ignoreFriendlyFire;
+
+// Stores what pairs of projectile classes that describe what (vulnerable)
+// class must be replaced with what (protected class). It also remembers the
+// previous state of `bGameRelevant` for replaceable class to restore it in
+// case this feature is disabled.
+struct ReplacementRule
+{
+ // `bGameRelevant` before this feature set it to `false`
+ var bool relevancyFlag;
+ var class vulnerableClass;
+ var class protectedClass;
+};
+var private const array rules;
+
+protected function OnEnabled()
+{
+ local int i;
+ for (i = 0; i < rules.length; i += 1)
+ {
+ if (rules[i].vulnerableClass == none) continue;
+ if (rules[i].protectedClass == none) continue;
+ rules[i].relevancyFlag = rules[i].vulnerableClass.default.bGameRelevant;
+ rules[i].vulnerableClass.default.bGameRelevant = false;
+ }
+ _.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement;
+}
+
+protected function OnDisabled()
+{
+ local int i;
+ for (i = 0; i < rules.length; i += 1)
+ {
+ if (rules[i].vulnerableClass == none) continue;
+ if (rules[i].protectedClass == none) continue;
+ rules[i].vulnerableClass.default.bGameRelevant = rules[i].relevancyFlag;
+ }
+ _.unreal.mutator.OnCheckReplacement(self).Disconnect();
+}
+
+protected function SwapConfig(FeatureConfig config)
+{
+ local FixProjectileFF newConfig;
+ newConfig = FixProjectileFF(config);
+ if (newConfig == none) {
+ return;
+ }
+ ignoreFriendlyFire = newConfig.ignoreFriendlyFire;
+ default.ignoreFriendlyFire = ignoreFriendlyFire;
+}
+
+private function bool CheckReplacement(Actor other, out byte isSuperRelevant)
+{
+ local ROBallisticProjectile projectile;
+ local class newClass;
+ projectile = ROBallisticProjectile(other);
+ if (projectile == none) {
+ return true;
+ }
+ projectile.bGameRelevant = true;
+ newClass = FindFixedClass(projectile.class);
+ if (newClass == none || newClass == projectile.class) {
+ return true;
+ }
+ ReplaceProjectileWith(ROBallisticProjectile(other), newClass);
+ return true;
+}
+
+private function ReplaceProjectileWith(
+ ROBallisticProjectile oldProjectile,
+ class replacementClass)
+{
+ local Pawn instigator;
+ local ROBallisticProjectile newProjectile;
+ if (oldProjectile == none) return;
+ if (replacementClass == none) return;
+ instigator = oldProjectile.instigator;
+ if (instigator == none) return;
+ newProjectile = instigator.Spawn( replacementClass,,,
+ oldProjectile.location,
+ oldProjectile.rotation);
+ if (newProjectile == none) return;
+
+ // Move projectile damage data to make sure new projectile deals
+ // exactly the same damage as the old one and the old one no longer
+ // deals any.
+ // Technically we only need to zero `oldProjectile` damage values for
+ // most weapons, since they are automatically the same for the
+ // new projectile.
+ // However `KFMod.HuskGun` is an exception that changes these values
+ // depending of the charge, so we need to either consider this as a
+ // special case or just move values all the time - which is what we have
+ // chosen to do.
+ newProjectile.damage = oldProjectile.damage;
+ oldProjectile.damage = 0;
+ newProjectile.damageRadius = oldProjectile.damageRadius;
+ oldProjectile.damageRadius = 0;
+ MoveImpactDamage(oldProjectile, newProjectile);
+ // New projectile must govern all of the mechanics, so make the old mimic
+ // former's movements as close as possible
+ SetupOldProjectileAsFace(oldProjectile, newProjectile);
+}
+
+// Make old projectile behave as close to the new one as possible
+private function SetupOldProjectileAsFace(
+ ROBallisticProjectile oldProjectile,
+ ROBallisticProjectile newProjectile)
+{
+ local FixProjectileFFClass_SPGrenadeProjectile newProjectileAsOrca;
+ local FixProjectileFFClass_SealSquealProjectile newProjectileAsHarpoon;
+ if (oldProjectile == none) return;
+ if (newProjectile == none) return;
+
+ // Removing collisions:
+ // 1. Avoids unnecessary bumping into zeds and other `Actor`s;
+ // 2. Removes `oldProjectile` from being accessible by
+ // `VisibleCollidingActors()`, therefore avoiding unwanted
+ // `TakeDamage()` calls from friendly fire.
+ oldProjectile.SetCollision(false, false);
+ // Prevent `oldProjectile` from dealing explosion damage in case something
+ // still damages and explodes it
+ oldProjectile.bHurtEntry = true;
+ // We can only make client-side projectile follow new server-side one for
+ // two projectile classes
+ newProjectileAsOrca =
+ FixProjectileFFClass_SPGrenadeProjectile(newProjectile);
+ if (newProjectileAsOrca != none)
+ {
+ newProjectileAsOrca.SetFace(SPGrenadeProjectile(oldProjectile));
+ return;
+ }
+ newProjectileAsHarpoon =
+ FixProjectileFFClass_SealSquealProjectile(newProjectile);
+ if (newProjectileAsHarpoon != none) {
+ newProjectileAsHarpoon.SetFace(SealSquealProjectile(oldProjectile));
+ }
+}
+
+// `impactDamage` is separately defined in 4 different base classes of interest
+// and moving (or zeroing) it is cumbersome enough to warrant a separate method
+private function MoveImpactDamage(
+ ROBallisticProjectile oldProjectile,
+ ROBallisticProjectile newProjectile)
+{
+ local LAWProj oldProjectileAsLaw, newProjectileAsLaw;
+ local M79GrenadeProjectile oldProjectileAsM79, newProjectileAsM79;
+ local SPGrenadeProjectile oldProjectileAsOrca, newProjectileAsOrca;
+ local SealSquealProjectile oldProjectileAsHarpoon, newProjectileAsHarpoon;
+ if (oldProjectile == none) return;
+ if (newProjectile == none) return;
+
+ // L.A.W + derivatives:
+ // Zed guns, Flare Revolver, Husk Gun
+ oldProjectileAsLaw = LawProj(oldProjectile);
+ newProjectileAsLaw = LawProj(newProjectile);
+ if (oldProjectileAsLaw != none && newProjectileAsLaw != none)
+ {
+ newProjectileAsLaw.impactDamage = oldProjectileAsLaw.impactDamage;
+ oldProjectileAsLaw.impactDamage = 0;
+ return;
+ }
+ // M79 Grenade Launcher + derivatives:
+ // M32, M4 203
+ oldProjectileAsM79 = M79GrenadeProjectile(oldProjectile);
+ newProjectileAsM79 = M79GrenadeProjectile(newProjectile);
+ if (oldProjectileAsM79 != none && newProjectileAsM79 != none)
+ {
+ newProjectileAsM79.impactDamage = oldProjectileAsM79.impactDamage;
+ oldProjectileAsM79.impactDamage = 0;
+ return;
+ }
+ // The Orca Bomb Propeller
+ oldProjectileAsOrca = SPGrenadeProjectile(oldProjectile);
+ newProjectileAsOrca = SPGrenadeProjectile(newProjectile);
+ if (oldProjectileAsOrca != none && newProjectileAsOrca != none)
+ {
+ newProjectileAsOrca.impactDamage = oldProjectileAsOrca.impactDamage;
+ oldProjectileAsOrca.impactDamage = 0;
+ return;
+ }
+ // SealSqueal Harpoon Bomber
+ oldProjectileAsHarpoon = SealSquealProjectile(oldProjectile);
+ newProjectileAsHarpoon = SealSquealProjectile(newProjectile);
+ if (oldProjectileAsHarpoon != none && newProjectileAsHarpoon != none)
+ {
+ newProjectileAsHarpoon.impactDamage =
+ oldProjectileAsHarpoon.impactDamage;
+ oldProjectileAsHarpoon.impactDamage = 0;
+ return;
+ }
+}
+
+// Returns "fixed" class that no longer explodes from random damage
+private final function class FindFixedClass(
+ class projectileClass)
+{
+ local int i;
+ local array rulesCopy;
+ if (projectileClass == none) {
+ return none;
+ }
+ rulesCopy = default.rules;
+ for (i = 0; i < rulesCopy.length; i += 1)
+ {
+ if (rulesCopy[i].vulnerableClass == projectileClass) {
+ return rulesCopy[i].protectedClass;
+ }
+ }
+ return projectileClass;
+}
+
+// Check if, according to this fix, projectiles should explode from
+// friendly fire
+// If `FixProjectileFF` in disabled always returns `false`.
+public static final function bool IsFriendlyFireAcceptable()
+{
+ if (default.ignoreFriendlyFire) {
+ return false;
+ }
+ return __().unreal.GetKFGameType().friendlyFireScale > 0;
+}
+
+defaultproperties
+{
+ configClass = class'FixProjectileFF'
+ ignoreFriendlyFire = false
+ rules(0) = (vulnerableClass=class'KFMod.M79GrenadeProjectile',protectedClass=class'FixProjectileFFClass_M79GrenadeProjectile')
+ rules(1) = (vulnerableClass=class'KFMod.M32GrenadeProjectile',protectedClass=class'FixProjectileFFClass_M32GrenadeProjectile')
+ rules(2) = (vulnerableClass=class'KFMod.LAWProj',protectedClass=class'FixProjectileFFClass_LAWProj')
+ rules(3) = (vulnerableClass=class'KFMod.M203GrenadeProjectile',protectedClass=class'FixProjectileFFClass_M203GrenadeProjectile')
+ rules(4) = (vulnerableClass=class'KFMod.ZEDGunProjectile',protectedClass=class'FixProjectileFFClass_ZEDGunProjectile')
+ rules(5) = (vulnerableClass=class'KFMod.ZEDMKIIPrimaryProjectile',protectedClass=class'FixProjectileFFClass_ZEDMKIIPrimaryProjectile')
+ rules(6) = (vulnerableClass=class'KFMod.ZEDMKIISecondaryProjectile',protectedClass=class'FixProjectileFFClass_ZEDMKIISecondaryProjectile')
+ rules(7) = (vulnerableClass=class'KFMod.FlareRevolverProjectile',protectedClass=class'FixProjectileFFClass_FlareRevolverProjectile')
+ rules(8) = (vulnerableClass=class'KFMod.HuskGunProjectile',protectedClass=class'FixProjectileFFClass_HuskGunProjectile')
+ rules(9) = (vulnerableClass=class'KFMod.SPGrenadeProjectile',protectedClass=class'FixProjectileFFClass_SPGrenadeProjectile')
+ rules(10) = (vulnerableClass=class'KFMod.SealSquealProjectile',protectedClass=class'FixProjectileFFClass_SealSquealProjectile')
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_FlareRevolverProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_FlareRevolverProjectile.uc
index 46b8831..86f7d31 100644
--- a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_FlareRevolverProjectile.uc
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_FlareRevolverProjectile.uc
@@ -38,7 +38,7 @@ function TakeDamage(
}
canTakeThisDamage =
(instigatedBy == instigator)
- || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ || class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
if (canTakeThisDamage && !bDud) {
Explode(hitLocation, Vect(0, 0, 0));
}
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_HuskGunProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_HuskGunProjectile.uc
index f79d77d..e9809a1 100644
--- a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_HuskGunProjectile.uc
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_HuskGunProjectile.uc
@@ -37,7 +37,7 @@ function TakeDamage(
}
canTakeThisDamage =
(instigatedBy == instigator)
- || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ || class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
if (canTakeThisDamage && !bDud) {
Explode(hitLocation, Vect(0, 0, 0));
}
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_LAWProj.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_LAWProj.uc
index d823df2..3155373 100644
--- a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_LAWProj.uc
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_LAWProj.uc
@@ -37,7 +37,7 @@ function TakeDamage(
}
canTakeThisDamage =
(instigatedBy == instigator)
- || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ || class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
if (canTakeThisDamage && !bDud) {
Explode(hitLocation, Vect(0, 0, 0));
}
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M203GrenadeProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M203GrenadeProjectile.uc
index 5664c19..d99a9df 100644
--- a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M203GrenadeProjectile.uc
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M203GrenadeProjectile.uc
@@ -37,7 +37,7 @@ function TakeDamage(
}
canTakeThisDamage =
(instigatedBy == instigator)
- || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ || class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
if (canTakeThisDamage && !bDud) {
Explode(hitLocation, Vect(0, 0, 0));
}
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M32GrenadeProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M32GrenadeProjectile.uc
index 8c5179c..f835b0d 100644
--- a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M32GrenadeProjectile.uc
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M32GrenadeProjectile.uc
@@ -37,7 +37,7 @@ function TakeDamage(
}
canTakeThisDamage =
(instigatedBy == instigator)
- || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ || class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
if (canTakeThisDamage && !bDud) {
Explode(hitLocation, Vect(0, 0, 0));
}
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M79GrenadeProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M79GrenadeProjectile.uc
index 9eaa34b..879261b 100644
--- a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M79GrenadeProjectile.uc
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M79GrenadeProjectile.uc
@@ -37,7 +37,7 @@ function TakeDamage(
}
canTakeThisDamage =
(instigatedBy == instigator)
- || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ || class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
if (canTakeThisDamage && !bDud) {
Explode(hitLocation, Vect(0, 0, 0));
}
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SPGrenadeProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SPGrenadeProjectile.uc
index f4b575c..4ca72d8 100644
--- a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SPGrenadeProjectile.uc
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SPGrenadeProjectile.uc
@@ -44,7 +44,7 @@ public function TakeDamage(
}
canTakeThisDamage =
(instigatedBy == instigator)
- || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ || class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
// Unlike M79/M32 - no `!bDud` check, since it's supposed to fall down
if (canTakeThisDamage)
{
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SealSquealProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SealSquealProjectile.uc
index 0799922..4595c52 100644
--- a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SealSquealProjectile.uc
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SealSquealProjectile.uc
@@ -44,7 +44,7 @@ public function TakeDamage(
}
canTakeThisDamage =
(instigatedBy == instigator)
- || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ || class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
// Unlike M79/M32 - no `!bDud` check, since it's supposed to fall down
if (canTakeThisDamage)
{
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDGunProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDGunProjectile.uc
index 2beb6bc..296d631 100644
--- a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDGunProjectile.uc
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDGunProjectile.uc
@@ -37,7 +37,7 @@ function TakeDamage(
}
canTakeThisDamage =
(instigatedBy == instigator)
- || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ || class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
if (canTakeThisDamage && !bDud) {
Explode(hitLocation, Vect(0, 0, 0));
}
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIIPrimaryProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIIPrimaryProjectile.uc
index a9ec190..d3d7cf1 100644
--- a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIIPrimaryProjectile.uc
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIIPrimaryProjectile.uc
@@ -38,7 +38,7 @@ function TakeDamage(
}
canTakeThisDamage =
(instigatedBy == instigator)
- || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ || class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
if (canTakeThisDamage && !bDud) {
Explode(hitLocation, Vect(0, 0, 0));
}
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIISecondaryProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIISecondaryProjectile.uc
index 673806e..fa907a8 100644
--- a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIISecondaryProjectile.uc
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIISecondaryProjectile.uc
@@ -37,7 +37,7 @@ function TakeDamage(
}
canTakeThisDamage =
(instigatedBy == instigator)
- || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ || class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
if (canTakeThisDamage && !bDud) {
Explode(hitLocation, Vect(0, 0, 0));
}
diff --git a/sources/FixProjectileFF/MutatorListener_FixProjectileFF.uc b/sources/FixProjectileFF/MutatorListener_FixProjectileFF.uc
deleted file mode 100644
index 95ba8d0..0000000
--- a/sources/FixProjectileFF/MutatorListener_FixProjectileFF.uc
+++ /dev/null
@@ -1,168 +0,0 @@
-/**
- * Overloaded mutator events listener to replace projectiles vulnerable to
- * friendly fire.
- * Copyright 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 .
- */
-class MutatorListener_FixProjectileFF extends MutatorListenerBase
- abstract;
-
-static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
-{
- local ROBallisticProjectile projectile;
- local class newClass;
- projectile = ROBallisticProjectile(other);
- if (projectile == none) return true;
-
- projectile.bGameRelevant = true;
- newClass =
- class'FixProjectileFF'.static.FindFixedClass(projectile.class);
- if (newClass == none || newClass == projectile.class) {
- return true;
- }
- ReplaceProjectileWith(ROBallisticProjectile(other), newClass);
- return true;
-}
-
-static function ReplaceProjectileWith(
- ROBallisticProjectile oldProjectile,
- class replacementClass)
-{
- local Pawn instigator;
- local ROBallisticProjectile newProjectile;
- if (oldProjectile == none) return;
- if (replacementClass == none) return;
- instigator = oldProjectile.instigator;
- if (instigator == none) return;
- newProjectile = instigator.Spawn( replacementClass,,,
- oldProjectile.location,
- oldProjectile.rotation);
- if (newProjectile == none) return;
-
- // Move projectile damage data to make sure new projectile deals
- // exactly the same damage as the old one and the old one no longer
- // deals any.
- // Technically we only need to zero `oldProjectile` damage values for
- // most weapons, since they are automatically the same for the
- // new projectile.
- // However `KFMod.HuskGun` is an exception that changes these values
- // depending of the charge, so we need to either consider this as a
- // special case or just move values all the time - which is what we have
- // chosen to do.
- newProjectile.damage = oldProjectile.damage;
- oldProjectile.damage = 0;
- newProjectile.damageRadius = oldProjectile.damageRadius;
- oldProjectile.damageRadius = 0;
- MoveImpactDamage(oldProjectile, newProjectile);
- // New projectile must govern all of the mechanics, so make the old mimic
- // former's movements as close as possible
- SetupOldProjectileAsFace(oldProjectile, newProjectile);
-}
-
-// Make old projectile behave as close to the new one as possible
-static function SetupOldProjectileAsFace(
- ROBallisticProjectile oldProjectile,
- ROBallisticProjectile newProjectile)
-{
- local FixProjectileFFClass_SPGrenadeProjectile newProjectileAsOrca;
- local FixProjectileFFClass_SealSquealProjectile newProjectileAsHarpoon;
- if (oldProjectile == none) return;
- if (newProjectile == none) return;
-
- // Removing collisions:
- // 1. Avoids unnecessary bumping into zeds and other `Actor`s;
- // 2. Removes `oldProjectile` from being accessible by
- // `VisibleCollidingActors()`, therefore avoiding unwanted
- // `TakeDamage()` calls from friendly fire.
- oldProjectile.SetCollision(false, false);
- // Prevent `oldProjectile` from dealing explosion damage in case something
- // still damages and explodes it
- oldProjectile.bHurtEntry = true;
- // We can only make client-side projectile follow new server-side one for
- // two projectile classes
- newProjectileAsOrca =
- FixProjectileFFClass_SPGrenadeProjectile(newProjectile);
- if (newProjectileAsOrca != none)
- {
- newProjectileAsOrca.SetFace(SPGrenadeProjectile(oldProjectile));
- return;
- }
- newProjectileAsHarpoon =
- FixProjectileFFClass_SealSquealProjectile(newProjectile);
- if (newProjectileAsHarpoon != none) {
- newProjectileAsHarpoon.SetFace(SealSquealProjectile(oldProjectile));
- }
-}
-
-// `impactDamage` is separately defined in 4 different base classes of interest
-// and moving (or zeroing) it is cumbersome enough to warrant a separate method
-static function MoveImpactDamage(
- ROBallisticProjectile oldProjectile,
- ROBallisticProjectile newProjectile)
-{
- local LAWProj oldProjectileAsLaw, newProjectileAsLaw;
- local M79GrenadeProjectile oldProjectileAsM79, newProjectileAsM79;
- local SPGrenadeProjectile oldProjectileAsOrca, newProjectileAsOrca;
- local SealSquealProjectile oldProjectileAsHarpoon, newProjectileAsHarpoon;
- if (oldProjectile == none) return;
- if (newProjectile == none) return;
-
- // L.A.W + derivatives:
- // Zed guns, Flare Revolver, Husk Gun
- oldProjectileAsLaw = LawProj(oldProjectile);
- newProjectileAsLaw = LawProj(newProjectile);
- if (oldProjectileAsLaw != none && newProjectileAsLaw != none)
- {
- newProjectileAsLaw.impactDamage = oldProjectileAsLaw.impactDamage;
- oldProjectileAsLaw.impactDamage = 0;
- return;
- }
- // M79 Grenade Launcher + derivatives:
- // M32, M4 203
- oldProjectileAsM79 = M79GrenadeProjectile(oldProjectile);
- newProjectileAsM79 = M79GrenadeProjectile(newProjectile);
- if (oldProjectileAsM79 != none && newProjectileAsM79 != none)
- {
- newProjectileAsM79.impactDamage = oldProjectileAsM79.impactDamage;
- oldProjectileAsM79.impactDamage = 0;
- return;
- }
- // The Orca Bomb Propeller
- oldProjectileAsOrca = SPGrenadeProjectile(oldProjectile);
- newProjectileAsOrca = SPGrenadeProjectile(newProjectile);
- if (oldProjectileAsOrca != none && newProjectileAsOrca != none)
- {
- newProjectileAsOrca.impactDamage = oldProjectileAsOrca.impactDamage;
- oldProjectileAsOrca.impactDamage = 0;
- return;
- }
- // SealSqueal Harpoon Bomber
- oldProjectileAsHarpoon = SealSquealProjectile(oldProjectile);
- newProjectileAsHarpoon = SealSquealProjectile(newProjectile);
- if (oldProjectileAsHarpoon != none && newProjectileAsHarpoon != none)
- {
- newProjectileAsHarpoon.impactDamage =
- oldProjectileAsHarpoon.impactDamage;
- oldProjectileAsHarpoon.impactDamage = 0;
- return;
- }
-}
-
-defaultproperties
-{
- relatedEvents = class'MutatorEvents'
-}
\ No newline at end of file
diff --git a/sources/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc b/sources/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc
deleted file mode 100644
index dc16c4a..0000000
--- a/sources/FixSpectatorCrash/BroadcastListener_FixSpectatorCrash.uc
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Overloaded broadcast events listener to catch the moment
- * someone becomes alive player / spectator.
- * 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 BroadcastListener_FixSpectatorCrash extends BroadcastListenerBase
- abstract;
-
-var private const int becomeAlivePlayerID;
-var private const int becomeSpectatorID;
-
-static function bool HandleLocalized(
- Actor sender,
- BroadcastEvents.LocalizedMessage message
-)
-{
- local FixSpectatorCrash specFix;
- local PlayerController senderController;
- if (sender == none) return true;
- if (sender.level == none || sender.level.game == none) return true;
- if (message.class != sender.level.game.gameMessageClass) return true;
- if ( message.id != default.becomeAlivePlayerID
- && message.id != default.becomeSpectatorID) return true;
-
- specFix = FixSpectatorCrash(class'FixSpectatorCrash'.static.GetInstance());
- senderController = GetController(sender);
- specFix.NotifyStatusChange(senderController);
- return (!specFix.IsViolator(senderController));
-}
-
-defaultproperties
-{
- becomeAlivePlayerID = 1
- becomeSpectatorID = 14
-}
\ No newline at end of file
diff --git a/sources/FixSpectatorCrash/FixSpectatorCrash.uc b/sources/FixSpectatorCrash/FixSpectatorCrash.uc
index 838c7ae..d898530 100644
--- a/sources/FixSpectatorCrash/FixSpectatorCrash.uc
+++ b/sources/FixSpectatorCrash/FixSpectatorCrash.uc
@@ -1,11 +1,6 @@
/**
- * This feature attempts to prevent server crashes caused by someone
- * quickly switching between being spectator and an active player.
- *
- * We do so by disconnecting players who start switching way too fast
- * (more than twice in a short amount of time) and temporarily faking a large
- * amount of players on the server, to prevent such spam from affecting the server.
- * Copyright 2020 Anton Tarasenko
+ * Config object for `FixSpectatorCrash_Feature`.
+ * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -22,276 +17,40 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class FixSpectatorCrash extends Feature
- dependson(ConnectionService)
+class FixSpectatorCrash extends FeatureConfig
+ perobjectconfig
config(AcediaFixes);
-/**
- * We use broadcast events to track when someone is switching
- * to active player or spectator and remember such people
- * for a short time (cooldown), defined by (`spectatorChangeTimeout`).
- * If one of the player we've remembered tries to switch again,
- * before the defined cooldown ran out, - we kick him
- * by destroying his controller.
- * One possible problem arises from the fact that controllers aren't
- * immediately destroyed and instead initiate player disconnection, -
- * exploiter might have enough time to cause a lag or even crash the server.
- * We address this issue by temporarily blocking anyone from
- * becoming active player (we do this by setting `numPlayers` variable in
- * killing floor's game info to a large value).
- * After all malicious players have successfully disconnected, -
- * we remove the block.
- */
-
-// This fix will try to kick any player that switches between active player
-// and cooldown faster than time (in seconds) in this value.
-// NOTE: raising this value past default value of `0.25`
-// won't actually improve crash prevention.
-var private config const float spectatorChangeTimeout;
-
-// [ADVANCED] Don't change this setting unless you know what you're doing.
-// Allows you to turn off server blocking.
-// Players that don't respect timeout will still be kicked.
-// This might be needed if this fix conflicts with another mutator
-// that also changes `numPlayers`.
-// However, it is necessary to block aggressive enough server crash attempts,
-// but can cause compatibility issues with some mutators.
-// It's highly preferred to rewrite such a mutator to be compatible.
-// NOTE: it should be compatible with most faked players-type mutators,
-// since this fix remembers the difference between amount of
-// real players and `numPlayers`.
-// After unblocking, it sets `numPlayers` to
-// the current amount of real players + that difference.
-// So 4 players + 3 (=7 numPlayers) after kicking 1 player becomes
-// 3 players + 3 (=6 numPlayers).
-var private config const bool allowServerBlock;
-
-// Stores remaining cooldown value before the next allowed
-// spectator change per player.
-struct CooldownRecord
-{
- // Reference to `PlayerController`
- var NativeActorRef player;
- var float cooldown;
-};
-
-// Currently active cooldowns
-var private array currentCooldowns;
-
-// Players who were decided to be violators and
-// were marked for disconnecting.
-// We'll be maintaining server block as long as even one
-// of them hasn't yet disconnected.
-// References to `PlayerController`s.
-var private array violators;
-
-// Is server currently blocked?
-var private bool becomingActiveBlocked;
-// This value introduced to accommodate mods such as faked player that can
-// change `numPlayers` to a value that isn't directly tied to the
-// current number of active players.
-// We remember the difference between active players and `numPlayers`
-/// variable in game type before server block and add it after block is over.
-// If some mod introduces a more complicated relation between amount of
-// active players and `numPlayers`, then it must take care of
-// compatibility on it's own.
-var private int recordedNumPlayersMod;
-
-protected function OnEnabled()
-{
- _.unreal.OnTick(self).connect = Tick;
-}
-
-protected function OnDisabled()
-{
- _.unreal.OnTick(self).Disconnect();
-}
-
-// If given `PlayerController` is registered in our cooldown records, -
-// returns it's index.
-// If it doesn't exists (or `none` value was passes), - returns `-1`.
-private final function int GetCooldownIndex(PlayerController player)
-{
- local int i;
- if (player == none) return -1;
-
- for (i = 0; i < currentCooldowns.length; i += 1)
- {
- if (currentCooldowns[i].player.Get() == player) {
- return i;
- }
- }
- return -1;
-}
-
-// Checks if given `PlayerController` is registered as a violator.
-// `none` value isn't a violator.
-public final function bool IsViolator(PlayerController player)
-{
- local int i;
- if (player == none) return false;
-
- for (i = 0; i < violators.length; i += 1)
- {
- if (violators[i].Get() == player) {
- return true;
- }
- }
- return false;
-}
-
-// This function is to notify our fix that some player just changed status
-// of active player / spectator.
-// If passes value isn't `none`, it puts given player on cooldown or kicks him.
-public final function NotifyStatusChange(PlayerController player)
-{
- local int index;
- local CooldownRecord newRecord;
- if (player == none) return;
-
- index = GetCooldownIndex(player);
- // Players already on cool down must be kicked and marked as violators
- if (index >= 0)
- {
- player.Destroy();
- violators[violators.length] = currentCooldowns[index].player;
- currentCooldowns.Remove(index, 1);
- if (allowServerBlock) {
- SetBlock(true);
- }
- }
- // Players that aren't on cooldown are
- // either violators (do nothing, just wait for their disconnect)
- // or didn't recently change their status (put them on cooldown).
- else if (!IsViolator(player))
- {
- newRecord.player = _.unreal.ActorRef(player);
- newRecord.cooldown = spectatorChangeTimeout;
- currentCooldowns[currentCooldowns.length] = newRecord;
- }
-}
-
-// Pass `true` to block server, `false` to unblock.
-// Only works if `allowServerBlock` is set to `true`.
-private final function SetBlock(bool activateBlock)
-{
- local KFGameType kfGameType;
- // Do we even need to do anything?
- if (!allowServerBlock) return;
- if (activateBlock == becomingActiveBlocked) return;
-
- // Actually block/unblock
- kfGameType = _.unreal.GetKFGameType();
- becomingActiveBlocked = activateBlock;
- if (activateBlock)
- {
- recordedNumPlayersMod = GetNumPlayersMod();
- // This value both can't realistically fall below
- // `kfGameType.maxPlayer` and won't overflow from random increase
- // in vanilla code.
- kfGameType.numPlayers = maxInt / 2;
- }
- else
- {
- // Adding `recordedNumPlayersMod` should prevent
- // faked players from breaking.
- kfGameType.numPlayers = GetRealPlayers() + recordedNumPlayersMod;
- }
-}
-
-// Performs server blocking if violators have disconnected.
-private final function TryUnblocking()
-{
- local int i;
- if (!allowServerBlock) return;
- if (!becomingActiveBlocked) return;
-
- for (i = 0; i < violators.length; i += 1)
- {
- if (violators[i].Get() != none) {
- return;
- }
- }
- _.memory.FreeMany(violators);
- violators.length = 0;
- SetBlock(false);
-}
-
-// Counts current amount of "real" active players
-// (connected to the server and not spectators).
-// Need `ConnectionService` to be running, otherwise return `-1`.
-private final function int GetRealPlayers()
-{
- // Auxiliary variables
- local int i;
- local int realPlayersAmount;
- local PlayerController player;
- // Information extraction
- local ConnectionService service;
- local array connections;
- service = ConnectionService(class'ConnectionService'.static.GetInstance());
- if (service == none) return -1;
-
- // Count non-spectators
- connections = service.GetActiveConnections();
- realPlayersAmount = 0;
- for (i = 0; i < connections.length; i += 1)
- {
- player = connections[i].controllerReference;
- if (player == none) continue;
- if (player.playerReplicationInfo == none) continue;
- if (!player.playerReplicationInfo.bOnlySpectator) {
- realPlayersAmount += 1;
- }
- }
- return realPlayersAmount;
-}
+var public config float spectatorChangeTimeout;
+var public config bool allowServerBlock;
-// Calculates difference between current amount of "real" active players
-// and `numPlayers` from `KFGameType`.
-// Most typically this difference will be non-zero when using
-// faked players-type mutators
-// (difference will be equal to the amount of faked players).
-private final function int GetNumPlayersMod()
+protected function AssociativeArray ToData()
{
- return _.unreal.GetKFGameType().numPlayers - GetRealPlayers();
+ local AssociativeArray data;
+ data = __().collections.EmptyAssociativeArray();
+ data.SetBool(P("allowServerBlock"), allowServerBlock, true);
+ data.SetFloat(P("spectatorChangeTimeout"), spectatorChangeTimeout, true);
+ return data;
}
-private final function ReduceCooldowns(float timePassed)
+protected function FromData(AssociativeArray source)
{
- local int i;
- i = 0;
- while (i < currentCooldowns.length)
- {
- currentCooldowns[i].cooldown -= timePassed;
- if ( currentCooldowns[i].player.Get() != none
- && currentCooldowns[i].cooldown > 0.0)
- {
- i += 1;
- }
- else
- {
- currentCooldowns[i].player.FreeSelf();
- currentCooldowns.Remove(i, 1);
- }
+ if (source == none) {
+ return;
}
+ allowServerBlock = source.GetBool(P("allowServerBlock"), true);
+ spectatorChangeTimeout = source.GetFloat(P("spectatorChangeTimeout"), 0.25);
}
-private function Tick(float delta, float tileDilationCoefficient)
+protected function DefaultIt()
{
- local float trueTimePassed;
- trueTimePassed = delta / tileDilationCoefficient;
- TryUnblocking();
- ReduceCooldowns(trueTimePassed);
+ spectatorChangeTimeout = 0.25;
+ allowServerBlock = true;
}
defaultproperties
{
- // Configurable variables
+ configName = "AcediaFixes"
spectatorChangeTimeout = 0.25
allowServerBlock = true
- // Inner variables
- becomingActiveBlocked = false
- // Listeners
- requiredListeners(0) = class'BroadcastListener_FixSpectatorCrash'
}
\ No newline at end of file
diff --git a/sources/FixSpectatorCrash/FixSpectatorCrash_Feature.uc b/sources/FixSpectatorCrash/FixSpectatorCrash_Feature.uc
new file mode 100644
index 0000000..e253958
--- /dev/null
+++ b/sources/FixSpectatorCrash/FixSpectatorCrash_Feature.uc
@@ -0,0 +1,341 @@
+/**
+ * This feature attempts to prevent server crashes caused by someone
+ * quickly switching between being spectator and an active player.
+ *
+ * We do so by disconnecting players who start switching way too fast
+ * (more than twice in a short amount of time) and temporarily faking a large
+ * amount of players on the server, to prevent such spam from affecting the server.
+ * 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 FixSpectatorCrash_Feature extends Feature
+ dependson(ConnectionService);
+
+/**
+ * We use broadcast events to track when someone is switching
+ * to active player or spectator and remember such people
+ * for a short time (cooldown), defined by (`spectatorChangeTimeout`).
+ * If one of the player we've remembered tries to switch again,
+ * before the defined cooldown ran out, - we kick him
+ * by destroying his controller.
+ * One possible problem arises from the fact that controllers aren't
+ * immediately destroyed and instead initiate player disconnection, -
+ * exploiter might have enough time to cause a lag or even crash the server.
+ * We address this issue by temporarily blocking anyone from
+ * becoming active player (we do this by setting `numPlayers` variable in
+ * killing floor's game info to a large value).
+ * After all malicious players have successfully disconnected, -
+ * we remove the block.
+ */
+
+// This fix will try to kick any player that switches between active player
+// and cooldown faster than time (in seconds) in this value.
+// NOTE: raising this value past default value of `0.25`
+// won't actually improve crash prevention.
+var private config float spectatorChangeTimeout;
+
+// [ADVANCED] Don't change this setting unless you know what you're doing.
+// Allows you to turn off server blocking.
+// Players that don't respect timeout will still be kicked.
+// This might be needed if this fix conflicts with another mutator
+// that also changes `numPlayers`.
+// However, it is necessary to block aggressive enough server crash attempts,
+// but can cause compatibility issues with some mutators.
+// It's highly preferred to rewrite such a mutator to be compatible.
+// NOTE: it should be compatible with most faked players-type mutators,
+// since this fix remembers the difference between amount of
+// real players and `numPlayers`.
+// After unblocking, it sets `numPlayers` to
+// the current amount of real players + that difference.
+// So 4 players + 3 (=7 numPlayers) after kicking 1 player becomes
+// 3 players + 3 (=6 numPlayers).
+var private config bool allowServerBlock;
+
+// Stores remaining cooldown value before the next allowed
+// spectator change per player.
+struct CooldownRecord
+{
+ // Reference to `PlayerController`
+ var NativeActorRef player;
+ var float cooldown;
+};
+
+// Currently active cooldowns
+var private array currentCooldowns;
+
+// Players who were decided to be violators and
+// were marked for disconnecting.
+// We'll be maintaining server block as long as even one
+// of them hasn't yet disconnected.
+// References to `PlayerController`s.
+var private array violators;
+
+// Is server currently blocked?
+var private bool becomingActiveBlocked;
+// This value introduced to accommodate mods such as faked player that can
+// change `numPlayers` to a value that isn't directly tied to the
+// current number of active players.
+// We remember the difference between active players and `numPlayers`
+/// variable in game type before server block and add it after block is over.
+// If some mod introduces a more complicated relation between amount of
+// active players and `numPlayers`, then it must take care of
+// compatibility on it's own.
+var private int recordedNumPlayersMod;
+
+// IDs of localized messages, ripped from sources
+var private const int becomeAlivePlayerID;
+var private const int becomeSpectatorID;
+
+protected function OnEnabled()
+{
+ _.unreal.OnTick(self).connect = Tick;
+ _.unreal.broadcasts.OnHandleLocalized(self).connect = HandleLocalized;
+}
+
+protected function OnDisabled()
+{
+ _.unreal.OnTick(self).Disconnect();
+ _.unreal.broadcasts.OnHandleLocalized(self).Disconnect();
+}
+
+protected function SwapConfig(FeatureConfig config)
+{
+ local FixSpectatorCrash newConfig;
+ newConfig = FixSpectatorCrash(config);
+ if (newConfig == none) {
+ return;
+ }
+ spectatorChangeTimeout = newConfig.spectatorChangeTimeout;
+ allowServerBlock = newConfig.allowServerBlock;
+}
+
+private function bool HandleLocalized(
+ Actor sender,
+ BroadcastAPI.LocalizedMessage message)
+{
+ local PlayerController senderController;
+ if (sender == none) return true;
+ if (sender.level == none || sender.level.game == none) return true;
+ if (message.class != sender.level.game.gameMessageClass) return true;
+ if ( message.id != default.becomeAlivePlayerID
+ && message.id != default.becomeSpectatorID) return true;
+
+ senderController = GetController(sender);
+ NotifyStatusChange(senderController);
+ return (!IsViolator(senderController));
+}
+
+private final function PlayerController GetController(Actor sender)
+{
+ local Pawn senderPawn;
+ senderPawn = Pawn(sender);
+ if (senderPawn != none) {
+ return PlayerController(senderPawn.controller);
+ }
+ return PlayerController(sender);
+}
+
+// If given `PlayerController` is registered in our cooldown records, -
+// returns it's index.
+// If it doesn't exists (or `none` value was passes), - returns `-1`.
+private final function int GetCooldownIndex(PlayerController player)
+{
+ local int i;
+ if (player == none) return -1;
+
+ for (i = 0; i < currentCooldowns.length; i += 1)
+ {
+ if (currentCooldowns[i].player.Get() == player) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+// Checks if given `PlayerController` is registered as a violator.
+// `none` value isn't a violator.
+public final function bool IsViolator(PlayerController player)
+{
+ local int i;
+ if (player == none) return false;
+
+ for (i = 0; i < violators.length; i += 1)
+ {
+ if (violators[i].Get() == player) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// This function is to notify our fix that some player just changed status
+// of active player / spectator.
+// If passes value isn't `none`, it puts given player on cooldown or kicks him.
+public final function NotifyStatusChange(PlayerController player)
+{
+ local int index;
+ local CooldownRecord newRecord;
+ if (player == none) return;
+
+ index = GetCooldownIndex(player);
+ // Players already on cool down must be kicked and marked as violators
+ if (index >= 0)
+ {
+ player.Destroy();
+ violators[violators.length] = currentCooldowns[index].player;
+ currentCooldowns.Remove(index, 1);
+ if (allowServerBlock) {
+ SetBlock(true);
+ }
+ }
+ // Players that aren't on cooldown are
+ // either violators (do nothing, just wait for their disconnect)
+ // or didn't recently change their status (put them on cooldown).
+ else if (!IsViolator(player))
+ {
+ newRecord.player = _.unreal.ActorRef(player);
+ newRecord.cooldown = spectatorChangeTimeout;
+ currentCooldowns[currentCooldowns.length] = newRecord;
+ }
+}
+
+// Pass `true` to block server, `false` to unblock.
+// Only works if `allowServerBlock` is set to `true`.
+private final function SetBlock(bool activateBlock)
+{
+ local KFGameType kfGameType;
+ // Do we even need to do anything?
+ if (!allowServerBlock) return;
+ if (activateBlock == becomingActiveBlocked) return;
+
+ // Actually block/unblock
+ kfGameType = _.unreal.GetKFGameType();
+ becomingActiveBlocked = activateBlock;
+ if (activateBlock)
+ {
+ recordedNumPlayersMod = GetNumPlayersMod();
+ // This value both can't realistically fall below
+ // `kfGameType.maxPlayer` and won't overflow from random increase
+ // in vanilla code.
+ kfGameType.numPlayers = maxInt / 2;
+ }
+ else
+ {
+ // Adding `recordedNumPlayersMod` should prevent
+ // faked players from breaking.
+ kfGameType.numPlayers = GetRealPlayers() + recordedNumPlayersMod;
+ }
+}
+
+// Performs server blocking if violators have disconnected.
+private final function TryUnblocking()
+{
+ local int i;
+ if (!allowServerBlock) return;
+ if (!becomingActiveBlocked) return;
+
+ for (i = 0; i < violators.length; i += 1)
+ {
+ if (violators[i].Get() != none) {
+ return;
+ }
+ }
+ _.memory.FreeMany(violators);
+ violators.length = 0;
+ SetBlock(false);
+}
+
+// Counts current amount of "real" active players
+// (connected to the server and not spectators).
+// Need `ConnectionService` to be running, otherwise return `-1`.
+private final function int GetRealPlayers()
+{
+ // Auxiliary variables
+ local int i;
+ local int realPlayersAmount;
+ local PlayerController player;
+ // Information extraction
+ local ConnectionService service;
+ local array connections;
+ service = ConnectionService(class'ConnectionService'.static.GetInstance());
+ if (service == none) return -1;
+
+ // Count non-spectators
+ connections = service.GetActiveConnections();
+ realPlayersAmount = 0;
+ for (i = 0; i < connections.length; i += 1)
+ {
+ player = connections[i].controllerReference;
+ if (player == none) continue;
+ if (player.playerReplicationInfo == none) continue;
+ if (!player.playerReplicationInfo.bOnlySpectator) {
+ realPlayersAmount += 1;
+ }
+ }
+ return realPlayersAmount;
+}
+
+// Calculates difference between current amount of "real" active players
+// and `numPlayers` from `KFGameType`.
+// Most typically this difference will be non-zero when using
+// faked players-type mutators
+// (difference will be equal to the amount of faked players).
+private final function int GetNumPlayersMod()
+{
+ return _.unreal.GetKFGameType().numPlayers - GetRealPlayers();
+}
+
+private final function ReduceCooldowns(float timePassed)
+{
+ local int i;
+ i = 0;
+ while (i < currentCooldowns.length)
+ {
+ currentCooldowns[i].cooldown -= timePassed;
+ if ( currentCooldowns[i].player.Get() != none
+ && currentCooldowns[i].cooldown > 0.0)
+ {
+ i += 1;
+ }
+ else
+ {
+ currentCooldowns[i].player.FreeSelf();
+ currentCooldowns.Remove(i, 1);
+ }
+ }
+}
+
+private function Tick(float delta, float tileDilationCoefficient)
+{
+ local float trueTimePassed;
+ trueTimePassed = delta / tileDilationCoefficient;
+ TryUnblocking();
+ ReduceCooldowns(trueTimePassed);
+}
+
+defaultproperties
+{
+ configClass = class'FixSpectatorCrash'
+ // Configurable variables
+ spectatorChangeTimeout = 0.25
+ allowServerBlock = true
+ // Inner variables
+ becomingActiveBlocked = false
+ // Ripped IDs of localized messages of interest
+ becomeAlivePlayerID = 1
+ becomeSpectatorID = 14
+}
\ No newline at end of file
diff --git a/sources/FixZedTimeLags/FixZedTimeLags.uc b/sources/FixZedTimeLags/FixZedTimeLags.uc
index 7813cc7..89a8e81 100644
--- a/sources/FixZedTimeLags/FixZedTimeLags.uc
+++ b/sources/FixZedTimeLags/FixZedTimeLags.uc
@@ -1,11 +1,6 @@
/**
- * This feature fixes lags caused by a zed time that can occur
- * on some maps when a lot of zeds are present at once.
- * As a side effect it also fixes an issue where during zed time speed up
- * `zedTimeSlomoScale` was assumed to be default value of `0.2`.
- * Now zed time will behave correctly with mods that
- * change `zedTimeSlomoScale`.
- * Copyright 2020 Anton Tarasenko
+ * Config object for `FixZedTimeLags_Feature`.
+ * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -22,159 +17,42 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class FixZedTimeLags extends Feature
- dependson(ConnectionService)
+class FixZedTimeLags extends FeatureConfig
+ perobjectconfig
config(AcediaFixes);
-/**
- * When zed time activates, game speed is immediately set to
- * `zedTimeSlomoScale` (0.2 by default), defined, like all other variables,
- * in `KFGameType`. Zed time lasts `zedTimeDuration` seconds (3.0 by default),
- * but during last `zedTimeDuration * 0.166` seconds (by default 0.498)
- * it starts to speed back up, causing game speed to update every tick.
- * This makes animations look more smooth when exiting zed-time;
- * however, updating speed every tick for that purpose seems like
- * an overkill and, combined with things like
- * increased tick rate, certain maps and raised zed limit,
- * it can lead to noticeable lags at the end of zed time.
- * To fix this issue we disable `Tick()` event in
- * `KFGameType` and then repeat that functionality in our own `Tick()` event,
- * but only perform game speed updates occasionally,
- * to make sure that overall amount of updates won't go over a limit,
- * that can be configured via `maxGameSpeedUpdatesAmount`
- * Author's test (looking really hard on clots' animations)
- * seem to suggest that there shouldn't be much visible difference if
- * we limit game speed updates to about 2 or 3.
- */
+var public config int maxGameSpeedUpdatesAmount;
+var public config bool disableTick;
-// Max amount of game speed updates during speed up phase
-// (actual amount of updates can't be larger than amount of ticks).
-// On servers with default 30 tick rate there's usually
-// about 13 updates total on vanilla game.
-// Values lower than 1 are treated like 1.
-var private config const int maxGameSpeedUpdatesAmount;
-// [ADVANCED] Don't change this setting unless you know what you're doing.
-// Compatibility setting that allows to keep `GameInfo`'s `Tick()` event
-// from being disabled.
-// Useful when running Acedia along with custom `GameInfo`
-// (that isn't `KFGameType`) that relies on `Tick()` event.
-// Note, however, that in order to keep this fix working properly,
-// it's on you to make sure `KFGameType.Tick()` logic isn't executed.
-var private config const bool disableTick;
-// Counts how much time is left until next update
-var private float updateCooldown;
-
-protected function OnEnabled()
+protected function AssociativeArray ToData()
{
- if (disableTick) {
- _.unreal.GetGameType().Disable('Tick');
- }
- _.unreal.OnTick(self).connect = Tick;
+ local AssociativeArray data;
+ data = __().collections.EmptyAssociativeArray();
+ data.SetBool(P("disableTick"), disableTick, true);
+ data.SetInt(P("maxGameSpeedUpdatesAmount"),
+ maxGameSpeedUpdatesAmount, true);
+ return data;
}
-protected function OnDisabled()
+protected function FromData(AssociativeArray source)
{
- if (disableTick) {
- _.unreal.GetGameType().Enable('Tick');
- }
- _.unreal.OnTick(self).Disconnect();
-}
-
-private function Tick(float delta, float timeDilationCoefficient)
-{
- local KFGameType gameType;
- local float trueTimePassed;
- gameType = _.unreal.GetKFGameType();
- if (!gameType.bZEDTimeActive) return;
- // Unfortunately we need to keep disabling `Tick()` probe function,
- // because it constantly gets enabled back and I don't know where
- // (maybe native code?); only really matters during zed time.
- if (disableTick) {
- gameType.Disable('Tick');
- }
- // How much real (not in-game) time has passed
- trueTimePassed = delta / timeDilationCoefficient;
- gameType.currentZEDTimeDuration -= trueTimePassed;
-
- // Handle speeding up phase
- if (gameType.bSpeedingBackUp) {
- DoSpeedBackUp(trueTimePassed, gameType);
- }
- else if (gameType.currentZEDTimeDuration < GetSpeedupDuration(gameType))
- {
- gameType.bSpeedingBackUp = true;
- updateCooldown = GetFullUpdateCooldown(gameType);
- TellClientsZedTimeEnds();
- DoSpeedBackUp(trueTimePassed, gameType);
- }
- // End zed time once it's duration has passed
- if (gameType.currentZEDTimeDuration <= 0)
- {
- gameType.bZEDTimeActive = false;
- gameType.bSpeedingBackUp = false;
- gameType.zedTimeExtensionsUsed = 0;
- gameType.SetGameSpeed(1.0);
- }
-}
-
-private final function TellClientsZedTimeEnds()
-{
- local int i;
- local KFPlayerController player;
- local ConnectionService service;
- local array connections;
- service = ConnectionService(class'ConnectionService'.static.GetInstance());
- if (service == none) return;
- connections = service.GetActiveConnections();
- for (i = 0; i < connections.length; i += 1)
- {
- player = KFPlayerController(connections[i].controllerReference);
- if (player != none)
- {
- // Play sound of leaving zed time
- player.ClientExitZedTime();
- }
- }
-}
-
-// This function is called every tick during speed up phase and manages
-// gradual game speed increase.
-private final function DoSpeedBackUp(float trueTimePassed, KFGameType gameType)
-{
- // Game speed will always be updated in our `Tick()` event
- // at the very end of the zed time.
- // The rest of the updates will be uniformly distributed
- // over the speed up duration.
-
- local float newGameSpeed;
- local float slowdownScale;
- if (maxGameSpeedUpdatesAmount <= 1) return;
- if (updateCooldown > 0.0)
- {
- updateCooldown -= trueTimePassed;
+ if (source == none) {
return;
}
- else {
- updateCooldown = GetFullUpdateCooldown(gameType);
- }
- slowdownScale =
- gameType.currentZEDTimeDuration / GetSpeedupDuration(gameType);
- newGameSpeed = Lerp(slowdownScale, 1.0, gameType.zedTimeSlomoScale);
- gameType.SetGameSpeed(newGameSpeed);
-}
-
-private final function float GetSpeedupDuration(KFGameType gameType)
-{
- return gameType.zedTimeDuration * 0.166;
+ disableTick = source.GetBool(P("disableTick"), true);
+ maxGameSpeedUpdatesAmount =
+ source.GetInt(P("maxGameSpeedUpdatesAmount"), 3);
}
-private final function float GetFullUpdateCooldown(KFGameType gameType)
+protected function DefaultIt()
{
- return GetSpeedupDuration(gameType) / maxGameSpeedUpdatesAmount;
+ maxGameSpeedUpdatesAmount = 3;
+ disableTick = true;
}
defaultproperties
{
+ configName = "AcediaFixes"
maxGameSpeedUpdatesAmount = 3
disableTick = true
}
\ No newline at end of file
diff --git a/sources/FixZedTimeLags/FixZedTimeLags_Feature.uc b/sources/FixZedTimeLags/FixZedTimeLags_Feature.uc
new file mode 100644
index 0000000..c0ada7b
--- /dev/null
+++ b/sources/FixZedTimeLags/FixZedTimeLags_Feature.uc
@@ -0,0 +1,191 @@
+/**
+ * This feature fixes lags caused by a zed time that can occur
+ * on some maps when a lot of zeds are present at once.
+ * As a side effect it also fixes an issue where during zed time speed up
+ * `zedTimeSlomoScale` was assumed to be default value of `0.2`.
+ * Now zed time will behave correctly with mods that
+ * change `zedTimeSlomoScale`.
+ * 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 FixZedTimeLags_Feature extends Feature
+ dependson(ConnectionService);
+
+/**
+ * When zed time activates, game speed is immediately set to
+ * `zedTimeSlomoScale` (0.2 by default), defined, like all other variables,
+ * in `KFGameType`. Zed time lasts `zedTimeDuration` seconds (3.0 by default),
+ * but during last `zedTimeDuration * 0.166` seconds (by default 0.498)
+ * it starts to speed back up, causing game speed to update every tick.
+ * This makes animations look more smooth when exiting zed-time;
+ * however, updating speed every tick for that purpose seems like
+ * an overkill and, combined with things like
+ * increased tick rate, certain maps and raised zed limit,
+ * it can lead to noticeable lags at the end of zed time.
+ * To fix this issue we disable `Tick()` event in
+ * `KFGameType` and then repeat that functionality in our own `Tick()` event,
+ * but only perform game speed updates occasionally,
+ * to make sure that overall amount of updates won't go over a limit,
+ * that can be configured via `maxGameSpeedUpdatesAmount`
+ * Author's test (looking really hard on clots' animations)
+ * seem to suggest that there shouldn't be much visible difference if
+ * we limit game speed updates to about 2 or 3.
+ */
+
+// Max amount of game speed updates during speed up phase
+// (actual amount of updates can't be larger than amount of ticks).
+// On servers with default 30 tick rate there's usually
+// about 13 updates total on vanilla game.
+// Values lower than 1 are treated like 1.
+var private config int maxGameSpeedUpdatesAmount;
+// [ADVANCED] Don't change this setting unless you know what you're doing.
+// Compatibility setting that allows to keep `GameInfo`'s `Tick()` event
+// from being disabled.
+// Useful when running Acedia along with custom `GameInfo`
+// (that isn't `KFGameType`) that relies on `Tick()` event.
+// Note, however, that in order to keep this fix working properly,
+// it's on you to make sure `KFGameType.Tick()` logic isn't executed.
+var private config bool disableTick;
+// Counts how much time is left until next update
+var private float updateCooldown;
+
+protected function OnEnabled()
+{
+ if (disableTick) {
+ _.unreal.GetGameType().Disable('Tick');
+ }
+ _.unreal.OnTick(self).connect = Tick;
+}
+
+protected function OnDisabled()
+{
+ if (disableTick) {
+ _.unreal.GetGameType().Enable('Tick');
+ }
+ _.unreal.OnTick(self).Disconnect();
+}
+
+protected function SwapConfig(FeatureConfig config)
+{
+ local FixZedTimeLags newConfig;
+ newConfig = FixZedTimeLags(config);
+ if (newConfig == none) {
+ return;
+ }
+ maxGameSpeedUpdatesAmount = newConfig.maxGameSpeedUpdatesAmount;
+ disableTick = newConfig.disableTick;
+}
+
+private function Tick(float delta, float timeDilationCoefficient)
+{
+ local KFGameType gameType;
+ local float trueTimePassed;
+ gameType = _.unreal.GetKFGameType();
+ if (!gameType.bZEDTimeActive) return;
+ // Unfortunately we need to keep disabling `Tick()` probe function,
+ // because it constantly gets enabled back and I don't know where
+ // (maybe native code?); only really matters during zed time.
+ if (disableTick) {
+ gameType.Disable('Tick');
+ }
+ // How much real (not in-game) time has passed
+ trueTimePassed = delta / timeDilationCoefficient;
+ gameType.currentZEDTimeDuration -= trueTimePassed;
+
+ // Handle speeding up phase
+ if (gameType.bSpeedingBackUp) {
+ DoSpeedBackUp(trueTimePassed, gameType);
+ }
+ else if (gameType.currentZEDTimeDuration < GetSpeedupDuration(gameType))
+ {
+ gameType.bSpeedingBackUp = true;
+ updateCooldown = GetFullUpdateCooldown(gameType);
+ TellClientsZedTimeEnds();
+ DoSpeedBackUp(trueTimePassed, gameType);
+ }
+ // End zed time once it's duration has passed
+ if (gameType.currentZEDTimeDuration <= 0)
+ {
+ gameType.bZEDTimeActive = false;
+ gameType.bSpeedingBackUp = false;
+ gameType.zedTimeExtensionsUsed = 0;
+ gameType.SetGameSpeed(1.0);
+ }
+}
+
+private final function TellClientsZedTimeEnds()
+{
+ local int i;
+ local KFPlayerController player;
+ local ConnectionService service;
+ local array connections;
+ service = ConnectionService(class'ConnectionService'.static.GetInstance());
+ if (service == none) return;
+ connections = service.GetActiveConnections();
+ for (i = 0; i < connections.length; i += 1)
+ {
+ player = KFPlayerController(connections[i].controllerReference);
+ if (player != none)
+ {
+ // Play sound of leaving zed time
+ player.ClientExitZedTime();
+ }
+ }
+}
+
+// This function is called every tick during speed up phase and manages
+// gradual game speed increase.
+private final function DoSpeedBackUp(float trueTimePassed, KFGameType gameType)
+{
+ // Game speed will always be updated in our `Tick()` event
+ // at the very end of the zed time.
+ // The rest of the updates will be uniformly distributed
+ // over the speed up duration.
+
+ local float newGameSpeed;
+ local float slowdownScale;
+ if (maxGameSpeedUpdatesAmount <= 1) return;
+ if (updateCooldown > 0.0)
+ {
+ updateCooldown -= trueTimePassed;
+ return;
+ }
+ else {
+ updateCooldown = GetFullUpdateCooldown(gameType);
+ }
+ slowdownScale =
+ gameType.currentZEDTimeDuration / GetSpeedupDuration(gameType);
+ newGameSpeed = Lerp(slowdownScale, 1.0, gameType.zedTimeSlomoScale);
+ gameType.SetGameSpeed(newGameSpeed);
+}
+
+private final function float GetSpeedupDuration(KFGameType gameType)
+{
+ return gameType.zedTimeDuration * 0.166;
+}
+
+private final function float GetFullUpdateCooldown(KFGameType gameType)
+{
+ return GetSpeedupDuration(gameType) / maxGameSpeedUpdatesAmount;
+}
+
+defaultproperties
+{
+ configClass = class'FixZedTimeLags'
+ maxGameSpeedUpdatesAmount = 3
+ disableTick = true
+}
\ No newline at end of file
diff --git a/sources/Manifest.uc b/sources/Manifest.uc
index 8f87fd2..dba95d5 100644
--- a/sources/Manifest.uc
+++ b/sources/Manifest.uc
@@ -22,15 +22,15 @@
defaultproperties
{
- features(0) = class'FixZedTimeLags'
- features(1) = class'FixDoshSpam'
- features(2) = class'FixFFHack'
- features(3) = class'FixInfiniteNades'
- features(4) = class'FixAmmoSelling'
- features(5) = class'FixSpectatorCrash'
- features(6) = class'FixDualiesCost'
- features(7) = class'FixInventoryAbuse'
- features(8) = class'FixProjectileFF'
- features(9) = class'FixPipes'
- features(10) = class'FixLogSpam'
+ features(0) = class'FixZedTimeLags_Feature'
+ features(1) = class'FixDoshSpam_Feature'
+ features(2) = class'FixFFHack_Feature'
+ features(3) = class'FixInfiniteNades_Feature'
+ features(4) = class'FixAmmoSelling_Feature'
+ features(5) = class'FixSpectatorCrash_Feature'
+ features(6) = class'FixDualiesCost_Feature'
+ features(7) = class'FixInventoryAbuse_Feature'
+ features(8) = class'FixProjectileFF_Feature'
+ features(9) = class'FixPipes_Feature'
+ features(10) = class'FixLogSpam_Feature'
}
\ No newline at end of file