Prepare fixtures
This commit is contained in:
parent
797e5ea192
commit
9c94356263
6021 changed files with 722805 additions and 22 deletions
83
kf_sources/AcediaFixes/Classes/AmmoPickupStalker.uc
Normal file
83
kf_sources/AcediaFixes/Classes/AmmoPickupStalker.uc
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* This actor attaches itself to the ammo boxes
|
||||
* and imitates their collision to let us detect when they're picked up.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class AmmoPickupStalker extends Actor;
|
||||
|
||||
// Ammo box this stalker is attached to.
|
||||
// If it is destroyed (not just picked up) - stalker must die too.
|
||||
var private KFAmmoPickup target;
|
||||
|
||||
// This variable is used to record if our `target` ammo box was in
|
||||
// active state (`Pickup`) last time we've checked.
|
||||
// We need this because ammo box's `Touch()` event can fire off first and
|
||||
// force the box to sleep before stalker could catch same event.
|
||||
// Without this variable we would have no way to know if player
|
||||
// simply walked near the place of a sleeping box or actually grabbed it.
|
||||
var private bool wasActive;
|
||||
|
||||
// Static function that spawns a new stalker for the given box.
|
||||
// Careful, as there's no checks for whether a stalker is
|
||||
// already attached to it.
|
||||
// Ensuring that is on the user of the function.
|
||||
public final static function StalkAmmoPickup(KFAmmoPickup newTarget)
|
||||
{
|
||||
local AmmoPickupStalker newStalker;
|
||||
if (newTarget == none) return;
|
||||
|
||||
newStalker = newTarget.Spawn(class'AmmoPickupStalker');
|
||||
newStalker.target = newTarget;
|
||||
newStalker.SetBase(newTarget);
|
||||
newStalker.SetCollision(true);
|
||||
newStalker.SetCollisionSize(newTarget.collisionRadius,
|
||||
newTarget.collisionHeight);
|
||||
}
|
||||
|
||||
event Touch(Actor other)
|
||||
{
|
||||
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_Feature(
|
||||
class'FixAmmoSelling_Feature'.static.GetEnabledInstance());
|
||||
if (ammoSellingFix != none) {
|
||||
ammoSellingFix.RecordAmmoPickup(Pawn(other), target);
|
||||
}
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
if (target != none) {
|
||||
wasActive = target.IsInState('Pickup');
|
||||
}
|
||||
else {
|
||||
Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
// Server-only, hidden
|
||||
remoteRole = ROLE_None
|
||||
bAlwaysRelevant = true
|
||||
drawType = DT_None
|
||||
}
|
||||
50
kf_sources/AcediaFixes/Classes/FixAmmoSelling.uc
Normal file
50
kf_sources/AcediaFixes/Classes/FixAmmoSelling.uc
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Config object for `FixAmmoSelling_Feature`.
|
||||
* Copyright 2021-2022 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSelling extends FeatureConfig
|
||||
perobjectconfig
|
||||
config(AcediaFixes);
|
||||
|
||||
var public config bool allowNegativeDosh;
|
||||
|
||||
protected function HashTable ToData()
|
||||
{
|
||||
local HashTable data;
|
||||
data = __().collections.EmptyHashTable();
|
||||
data.SetBool(P("allowNegativeDosh"), allowNegativeDosh);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected function FromData(HashTable source)
|
||||
{
|
||||
if (source != none) {
|
||||
allowNegativeDosh = source.GetBool(P("allowNegativeDosh"));
|
||||
}
|
||||
}
|
||||
|
||||
protected function DefaultIt()
|
||||
{
|
||||
allowNegativeDosh = false;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaFixes"
|
||||
allowNegativeDosh = false
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* A helper class for `FixAmmoSelling` that sets ammo cost for M32 to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSellingClass_CamoM32Pickup extends CamoM32Pickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 42
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* A helper class for `FixAmmoSelling` that sets ammo cost for xbow to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSellingClass_CrossbowPickup extends CrossbowPickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 11.6
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* A helper class for `FixAmmoSelling` that sets ammo cost for m79 to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSellingClass_GoldenM79Pickup extends GoldenM79Pickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 7
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* A helper class for `FixAmmoSelling` that sets ammo cost for LAW to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSellingClass_LAWPickup extends LAWPickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 21
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* A helper class for `FixAmmoSelling` that sets ammo cost for M32 to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSellingClass_M32Pickup extends M32Pickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 42
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* A helper class for `FixAmmoSelling` that sets ammo cost for M79 to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSellingClass_M79Pickup extends M79Pickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 7
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* A helper class for `FixAmmoSelling` that sets ammo cost for pipes
|
||||
* to that of a level 6 player and doesn't allow for a perk discount.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSellingClass_PipeBombPickup extends PipeBombPickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 195
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* A helper class for `FixAmmoSelling` that sets ammo cost for
|
||||
* orca grnade launcher to that of a level 6 player
|
||||
* and doesn't allow for a perk discount.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSellingClass_SPGrenadePickup extends SPGrenadePickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 7
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* A helper class for `FixAmmoSelling` that sets ammo cost for harpoon
|
||||
* to that of a level 6 player and doesn't allow for a perk discount.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSellingClass_SealSquealPickup extends SealSquealPickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 21
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* A helper class for `FixAmmoSelling` that sets ammo cost for seeker
|
||||
* to that of a level 6 player and doesn't allow for a perk discount.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSellingClass_SeekerSixPickup extends SeekerSixPickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 10.5
|
||||
}
|
||||
72
kf_sources/AcediaFixes/Classes/FixAmmoSellingService.uc
Normal file
72
kf_sources/AcediaFixes/Classes/FixAmmoSellingService.uc
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* Service for storing `FixAmmoSelling`'s `Actor`s.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixAmmoSellingService extends FeatureService
|
||||
dependson(FixAmmoSelling_Feature);
|
||||
|
||||
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<FixAmmoSelling_Feature.WeaponRecord> registeredWeapons;
|
||||
|
||||
protected function Finalizer()
|
||||
{
|
||||
ammoSellingFix = none;
|
||||
}
|
||||
|
||||
public function SetOwnerFeature(Feature newOwnerFeature)
|
||||
{
|
||||
super.SetOwnerFeature(newOwnerFeature);
|
||||
ammoSellingFix = FixAmmoSelling_Feature(newOwnerFeature);
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
local int i;
|
||||
if (ammoSellingFix == none) {
|
||||
return;
|
||||
}
|
||||
// For all the weapon records...
|
||||
i = 0;
|
||||
while (i < registeredWeapons.length)
|
||||
{
|
||||
// ...remove dead records
|
||||
if (registeredWeapons[i].weapon == none)
|
||||
{
|
||||
registeredWeapons.Remove(i, 1);
|
||||
continue;
|
||||
}
|
||||
// ...find ammo if it's missing
|
||||
if (registeredWeapons[i].ammo == none)
|
||||
{
|
||||
registeredWeapons[i] =
|
||||
ammoSellingFix.FindAmmoInstance(registeredWeapons[i]);
|
||||
}
|
||||
// ...tax for ammo, if we can
|
||||
registeredWeapons[i] =
|
||||
ammoSellingFix.TaxAmmoChange(registeredWeapons[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
447
kf_sources/AcediaFixes/Classes/FixAmmoSelling_Feature.uc
Normal file
447
kf_sources/AcediaFixes/Classes/FixAmmoSelling_Feature.uc
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<KFWeapon> abusableWeapon;
|
||||
var class<KFWeaponPickup> pickupReplacement;
|
||||
};
|
||||
|
||||
// Actual list of abusable weapons.
|
||||
var private const array<ReplacementRule> 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 = _server.unreal.GetLevel();
|
||||
_server.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<AmmoPickupStalker> stalkers;
|
||||
local array<WeaponRecord> registeredWeapons;
|
||||
level = _server.unreal.GetLevel();
|
||||
_server.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<Actor> 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<WeaponRecord> 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<KFWeaponPickup> vanillaPickupClass, fixPickupClass;
|
||||
if (kfWeapon == none || kfWeapon.instigator == none) return 0.0;
|
||||
fixPickupClass = class<KFWeaponPickup>(kfWeapon.pickupClass);
|
||||
vanillaPickupClass = class<KFWeaponPickup>(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<WeaponRecord> 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<Inventory> 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'
|
||||
}
|
||||
66
kf_sources/AcediaFixes/Classes/FixDoshSpam.uc
Normal file
66
kf_sources/AcediaFixes/Classes/FixDoshSpam.uc
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Config object for `FixDoshSpam_Feature`.
|
||||
* Copyright 2021-2022 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixDoshSpam extends FeatureConfig
|
||||
perobjectconfig
|
||||
config(AcediaFixes);
|
||||
|
||||
var public config float doshPerSecondLimitMax;
|
||||
var public config float doshPerSecondLimitMin;
|
||||
var public config int criticalDoshAmount;
|
||||
var public config float checkInterval;
|
||||
|
||||
protected function HashTable ToData()
|
||||
{
|
||||
local HashTable data;
|
||||
data = __().collections.EmptyHashTable();
|
||||
data.SetFloat(P("doshPerSecondLimitMax"), doshPerSecondLimitMax);
|
||||
data.SetFloat(P("doshPerSecondLimitMin"), doshPerSecondLimitMin);
|
||||
data.SetInt(P("criticalDoshAmount"), criticalDoshAmount);
|
||||
data.SetFloat(P("checkInterval"), checkInterval);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected function FromData(HashTable source)
|
||||
{
|
||||
if (source != none)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
protected function DefaultIt()
|
||||
{
|
||||
doshPerSecondLimitMax = 50;
|
||||
doshPerSecondLimitMin = 5;
|
||||
criticalDoshAmount = 25;
|
||||
checkInterval = 0.25;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaFixes"
|
||||
doshPerSecondLimitMax = 50
|
||||
doshPerSecondLimitMin = 5
|
||||
criticalDoshAmount = 25
|
||||
checkInterval = 0.25
|
||||
}
|
||||
298
kf_sources/AcediaFixes/Classes/FixDoshSpam_Feature.uc
Normal file
298
kf_sources/AcediaFixes/Classes/FixDoshSpam_Feature.uc
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<DoshStreamPerPlayer> currentContributors;
|
||||
|
||||
// Wads of cash that are lying around on the map.
|
||||
var private array<NativeActorRef> wads;
|
||||
|
||||
// Generates "reset" events when `wads` array is getting cleaned from
|
||||
// destroyed/picked up dosh and players' contributions are reduced.
|
||||
var private Timer checkTimer;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
local LevelInfo level;
|
||||
local CashPickup nextCash;
|
||||
checkTimer = _server.time.StartRealTimer(checkInterval, true);
|
||||
checkTimer.OnElapsed(self).connect = Tick;
|
||||
_server.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement;
|
||||
level = _server.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] = _server.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;
|
||||
_server.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] = _server.unreal.ActorRef(cash);
|
||||
// Add contribution to player
|
||||
playerIndex = GetContributorIndex(player);
|
||||
if (playerIndex >= 0)
|
||||
{
|
||||
currentContributors[playerIndex].contribution += 1.0;
|
||||
return;
|
||||
}
|
||||
newStreamRecord.player = _server.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<DoshStreamPerPlayer> 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
|
||||
}
|
||||
52
kf_sources/AcediaFixes/Classes/FixDualiesCost.uc
Normal file
52
kf_sources/AcediaFixes/Classes/FixDualiesCost.uc
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Config object for `FixDualiesCost_Feature`.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixDualiesCost extends FeatureConfig
|
||||
perobjectconfig
|
||||
config(AcediaFixes);
|
||||
|
||||
var public config bool allowSellValueIncrease;
|
||||
|
||||
protected function HashTable ToData()
|
||||
{
|
||||
local HashTable data;
|
||||
data = __().collections.EmptyHashTable();
|
||||
data.SetBool(P("allowSellValueIncrease"), allowSellValueIncrease);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected function FromData(HashTable source)
|
||||
{
|
||||
if (source != none)
|
||||
{
|
||||
allowSellValueIncrease =
|
||||
source.GetBool(P("allowSellValueIncrease"), true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function DefaultIt()
|
||||
{
|
||||
allowSellValueIncrease = true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaFixes"
|
||||
allowSellValueIncrease = true
|
||||
}
|
||||
485
kf_sources/AcediaFixes/Classes/FixDualiesCost_Feature.uc
Normal file
485
kf_sources/AcediaFixes/Classes/FixDualiesCost_Feature.uc
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixDualiesCost_Feature extends Feature;
|
||||
|
||||
/**
|
||||
* Issues with pistols' cost may look varied and surface in
|
||||
* a plethora of ways, but all of them originate from the two main errors
|
||||
* in vanilla's code:
|
||||
* 1. If you have a pistol in your inventory at the time when you
|
||||
* buy/pickup another one - the sell value of resulting dualies is
|
||||
* incorrectly set to the sell value of the second pistol;
|
||||
* 2. When player has dual pistols and drops one on the floor, -
|
||||
* the sell value for the one left with the player isn't set.
|
||||
* All weapons in Killing Floor get sell value assigned to them
|
||||
* (appropriately, in a `SellValue` variable). This is to ensure that the sell
|
||||
* price is set the moment players buys the gun. Otherwise, due to ridiculous
|
||||
* perked discounts, you'd be able to buy a pistol at 30% price
|
||||
* as sharpshooter, but sell at 75% of a price as any other perk,
|
||||
* resulting in 45% of pure profit.
|
||||
* Unfortunately, that's exactly what happens when `SellValue` isn't set
|
||||
* (left as it's default value of `-1`): sell value of such weapons is
|
||||
* determined only at the moment of sale and depends on the perk of the seller,
|
||||
* allowing for possible exploits.
|
||||
*
|
||||
* These issues are fixed by directly assigning
|
||||
* proper values to `SellValue`. To do that we need to detect when player
|
||||
* buys/sells/drops/picks up weapons, which we accomplish by catching
|
||||
* `CheckReplacement()` event for weapon instances. This approach has two
|
||||
* issues.
|
||||
* One is that, if vanilla's code sets an incorrect sell value, -
|
||||
* it's doing it after weapon is spawned and, therefore,
|
||||
* after `CheckReplacement()` call, so we have, instead, to remember to do
|
||||
* it later, as early as possible
|
||||
* (either the next tick or before another operation with weapons).
|
||||
* Another issue is that when you have a pistol and pick up a pistol of
|
||||
* the same type, - at the moment dualies instance is spawned,
|
||||
* the original pistol in player's inventory is gone and we can't use
|
||||
* it's sell value to calculate new value of dual pistols.
|
||||
* This problem is solved by separately recording the value for every
|
||||
* single pistol every tick.
|
||||
* However, if pistol pickups are placed close enough together on the map,
|
||||
* player can start touching them (which triggers a pickup) at the same time,
|
||||
* picking them both in a single tick. This leaves us no room to record
|
||||
* the value of a single pistol players picks up first.
|
||||
* To get it we use game rules to catch `OverridePickupQuery` event that's
|
||||
* called before the first one gets destroyed,
|
||||
* but after it's sell value was already set.
|
||||
* Last issue is that when player picks up a second pistol - we don't know
|
||||
* it's sell value and, therefore, can't calculate value of dual pistols.
|
||||
* This is resolved by recording that value directly from a pickup,
|
||||
* in abovementioned function `OverridePickupQuery`.
|
||||
* NOTE: 9mm is an exception due to the fact that you always have at least
|
||||
* one and the last one can't be sold. We'll deal with it by setting
|
||||
* the following rule: sell value of the un-droppable pistol is always 0
|
||||
* and the value of a pair of 9mms is the value of the single droppable pistol.
|
||||
*/
|
||||
|
||||
// Some issues involve possible decrease in pistols' price and
|
||||
// don't lead to exploit, but are still bugs and require fixing.
|
||||
// If you have a Deagle in your inventory and then get another one
|
||||
// (by either buying or picking it off the ground) - the price of resulting
|
||||
// dual pistols will be set to the price of the last deagle,
|
||||
// like the first one wasn't worth anything at all.
|
||||
// In particular this means that (prices are off-perk for more clarity):
|
||||
// 1. If you buy dual deagles (-1000 do$h) and then sell them at 75% of
|
||||
// the cost (+750 do$h), you lose 250 do$h;
|
||||
// 2. If you first buy a deagle (-500 do$h), then buy
|
||||
// the second one (-500 do$h) and then sell them, you'll only get
|
||||
// 75% of the cost of 1 deagle (+375 do$h), now losing 625 do$h;
|
||||
// 3. So if you already have bought a deagle (-500 do$h),
|
||||
// you can get a more expensive weapon by doing a stupid thing
|
||||
// and first selling your Deagle (+375 do$h),
|
||||
// then buying dual deagles (-1000 do$h).
|
||||
// If you sell them after that, you'll gain 75% of the cost of
|
||||
// dual deagles (+750 do$h), leaving you with losing only 375 do$h.
|
||||
// Of course, situations described above are only relevant if you're planning
|
||||
// to sell your weapons at some point and most people won't even notice it.
|
||||
// But such an oversight still shouldn't exist in a game and we fix it by
|
||||
// setting sell value of dualies as a sum of values of each pistol.
|
||||
// Yet, fixing this issue leads to players having more expensive
|
||||
// (while fairly priced) weapons than on vanilla, technically making
|
||||
// the game easier. And some people might object to having that in
|
||||
// a whitelisted bug-fixing feature.
|
||||
// These people are, without a question, complete degenerates.
|
||||
// But making mods for only non-mentally challenged isn't inclusive.
|
||||
// So we add this option.
|
||||
// Set it to `false` if you only want to fix ammo printing
|
||||
// and leave the rest of the bullshit as-is.
|
||||
var private /*config*/ bool allowSellValueIncrease;
|
||||
|
||||
// Describe all the possible pairs of dual pistols in a vanilla game.
|
||||
struct DualiesPair
|
||||
{
|
||||
var class<KFWeapon> single;
|
||||
var class<KFWeapon> dual;
|
||||
};
|
||||
var private const array<DualiesPair> dualiesClasses;
|
||||
|
||||
// Describe sell values that need to be applied at earliest later point.
|
||||
struct WeaponValuePair
|
||||
{
|
||||
// Reference to `KFWeapon` instance
|
||||
var NativeActorRef weapon;
|
||||
var float value;
|
||||
};
|
||||
var private const array<WeaponValuePair> pendingValues;
|
||||
|
||||
// Describe sell values of all currently existing single pistols.
|
||||
struct WeaponDataRecord
|
||||
{
|
||||
// Reference to `KFWeapon` instance
|
||||
var NativeActorRef reference;
|
||||
var class<KFWeapon> class;
|
||||
var float value;
|
||||
// The whole point of this structure is to remember value of a weapon
|
||||
// after it's destroyed. Since `reference` will become `none` by then,
|
||||
// we will use the `owner` reference to identify the weapon.
|
||||
// Reference to `Pawn`.
|
||||
var NativeActorRef owner;
|
||||
};
|
||||
var private const array<WeaponDataRecord> storedValues;
|
||||
|
||||
// Sell value of the last seen pickup in `OverridePickupQuery`
|
||||
var private int nextSellValue;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
local LevelInfo level;
|
||||
local KFWeapon nextWeapon;
|
||||
_server.unreal.OnTick(self).connect = Tick;
|
||||
_server.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement;
|
||||
_server.unreal.gameRules.OnOverridePickupQuery(self).connect = PickupQuery;
|
||||
level = _server.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;
|
||||
_server.unreal.OnTick(self).Disconnect();
|
||||
_server.unreal.mutator.OnCheckReplacement(self).Disconnect();
|
||||
_server.unreal.gameRules.OnOverridePickupQuery(self).Disconnect();
|
||||
for (i = 0; i < storedValues.length; i += 1)
|
||||
{
|
||||
storedValues[i].reference.FreeSelf();
|
||||
storedValues[i].owner.FreeSelf();
|
||||
}
|
||||
for (i = 0; i < pendingValues.length; i += 1) {
|
||||
pendingValues[i].weapon.FreeSelf();
|
||||
}
|
||||
}
|
||||
|
||||
protected function SwapConfig(FeatureConfig config)
|
||||
{
|
||||
local FixDualiesCost newConfig;
|
||||
newConfig = FixDualiesCost(config);
|
||||
if (newConfig == none) {
|
||||
return;
|
||||
}
|
||||
allowSellValueIncrease = newConfig.allowSellValueIncrease;
|
||||
}
|
||||
|
||||
private final function bool CheckReplacement(
|
||||
Actor other,
|
||||
out byte isSuperRelevant)
|
||||
{
|
||||
local KFWeapon weapon;
|
||||
weapon = KFWeapon(other);
|
||||
if (weapon == none) {
|
||||
return true;
|
||||
}
|
||||
RegisterSinglePistol(weapon, true);
|
||||
FixCostAfterThrow(weapon);
|
||||
FixCostAfterBuying(weapon);
|
||||
FixCostAfterPickUp(weapon);
|
||||
return true;
|
||||
}
|
||||
|
||||
function bool PickupQuery(
|
||||
Pawn other,
|
||||
Pickup item,
|
||||
out byte allowPickup)
|
||||
{
|
||||
local KFWeaponPickup weaponPickup;
|
||||
weaponPickup = KFWeaponPickup(item);
|
||||
if (weaponPickup != none)
|
||||
{
|
||||
ApplyPendingValues();
|
||||
StoreSinglePistolValues();
|
||||
nextSellValue = weaponPickup.sellValue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Finds a weapon of a given class in given `Pawn`'s inventory.
|
||||
// Returns `none` if weapon isn't there.
|
||||
private final function KFWeapon GetWeaponOfClass(
|
||||
Pawn playerPawn,
|
||||
class<KFWeapon> weaponClass
|
||||
)
|
||||
{
|
||||
local Inventory invIter;
|
||||
if (playerPawn == none) return none;
|
||||
|
||||
invIter = playerPawn.inventory;
|
||||
while (invIter != none)
|
||||
{
|
||||
if (invIter.class == weaponClass) {
|
||||
return KFWeapon(invIter);
|
||||
}
|
||||
invIter = invIter.inventory;
|
||||
}
|
||||
return none;
|
||||
}
|
||||
|
||||
// Gets weapon index in our record of dual pistol classes.
|
||||
// Second variable determines whether we're searching for single
|
||||
// or dual variant:
|
||||
// ~ `true` - searching for single
|
||||
// ~ `false` - for dual
|
||||
// Returns `-1` if weapon isn't found
|
||||
// (dual MK23 won't be found as a single weapon).
|
||||
private final function int GetIndexAs(KFWeapon weapon, bool asSingle)
|
||||
{
|
||||
local int i;
|
||||
if (weapon == none) return -1;
|
||||
|
||||
for (i = 0; i < dualiesClasses.length; i += 1)
|
||||
{
|
||||
if (asSingle && dualiesClasses[i].single == weapon.class) {
|
||||
return i;
|
||||
}
|
||||
if (!asSingle && dualiesClasses[i].dual == weapon.class) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calculates full cost of a weapon with a discount,
|
||||
// dependent on it's instigator's perk.
|
||||
private final function float GetFullCost(KFWeapon weapon)
|
||||
{
|
||||
local float cost;
|
||||
local class<KFWeaponPickup> pickupClass;
|
||||
local KFPlayerReplicationInfo instigatorRI;
|
||||
if (weapon == none) return 0.0;
|
||||
pickupClass = class<KFWeaponPickup>(weapon.default.pickupClass);
|
||||
if (pickupClass == none) return 0.0;
|
||||
|
||||
cost = pickupClass.default.cost;
|
||||
if (weapon.instigator != none)
|
||||
{
|
||||
instigatorRI =
|
||||
KFPlayerReplicationInfo(weapon.instigator.playerReplicationInfo);
|
||||
}
|
||||
if (instigatorRI != none && instigatorRI.clientVeteranSkill != none)
|
||||
{
|
||||
cost *= instigatorRI.clientVeteranSkill.static
|
||||
.GetCostScaling(instigatorRI, pickupClass);
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
// If passed weapon is a pistol - we start tracking it's value;
|
||||
// Otherwise - do nothing.
|
||||
public final function RegisterSinglePistol(
|
||||
KFWeapon singlePistol,
|
||||
bool justSpawned
|
||||
)
|
||||
{
|
||||
local WeaponDataRecord newRecord;
|
||||
if (singlePistol == none) return;
|
||||
if (GetIndexAs(singlePistol, true) < 0) return;
|
||||
|
||||
newRecord.reference = _server.unreal.ActorRef(singlePistol);
|
||||
newRecord.class = singlePistol.class;
|
||||
newRecord.owner = _server.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 = _server.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 = _server.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')
|
||||
}
|
||||
163
kf_sources/AcediaFixes/Classes/FixFFHack.uc
Normal file
163
kf_sources/AcediaFixes/Classes/FixFFHack.uc
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* Config object for `FixFFHack_Feature`.
|
||||
* Copyright 2021-2022 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixFFHack extends FeatureConfig
|
||||
perobjectconfig
|
||||
config(AcediaFixes);
|
||||
|
||||
var public config bool scaleByDefault;
|
||||
var public config array< class<DamageType> > alwaysScale;
|
||||
var public config array< class<DamageType> > neverScale;
|
||||
|
||||
protected function HashTable ToData()
|
||||
{
|
||||
local int i;
|
||||
local HashTable data;
|
||||
local ArrayList damageTypeArray;
|
||||
data = _.collections.EmptyHashTable();
|
||||
data.SetBool(P("scaleByDefault"), scaleByDefault);
|
||||
damageTypeArray = _.collections.EmptyArrayList();
|
||||
for (i = 0; i < alwaysScale.length; i += 1) {
|
||||
damageTypeArray.AddString(string(alwaysScale[i]));
|
||||
}
|
||||
data.SetItem(P("alwaysScale"), damageTypeArray);
|
||||
_.memory.Free(damageTypeArray);
|
||||
damageTypeArray = _.collections.EmptyArrayList();
|
||||
for (i = 0; i < neverScale.length; i += 1) {
|
||||
damageTypeArray.AddString(string(neverScale[i]));
|
||||
}
|
||||
data.SetItem(P("neverScale"), damageTypeArray);
|
||||
_.memory.Free(damageTypeArray);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected function FromData(HashTable source)
|
||||
{
|
||||
local int i;
|
||||
local ArrayList damageTypeArray;
|
||||
if (source == none) {
|
||||
return;
|
||||
}
|
||||
scaleByDefault = source.GetBool(P("scaleByDefault"));
|
||||
alwaysScale.length = 0;
|
||||
damageTypeArray = source.GetArrayList(P("alwaysScale"));
|
||||
if (damageTypeArray != none) {
|
||||
for (i = 0; i < damageTypeArray.GetLength(); i += 1)
|
||||
{
|
||||
alwaysScale[i] = class<DamageType>(
|
||||
_.memory.LoadClass_S(damageTypeArray.GetString(i)));
|
||||
}
|
||||
}
|
||||
_.memory.Free(damageTypeArray);
|
||||
neverScale.length = 0;
|
||||
damageTypeArray = source.GetArrayList(P("neverScale"));
|
||||
if (damageTypeArray != none) {
|
||||
for (i = 0; i < damageTypeArray.GetLength(); i += 1)
|
||||
{
|
||||
neverScale[i] = class<DamageType>(
|
||||
_.memory.LoadClass_S(damageTypeArray.GetString(i)));
|
||||
}
|
||||
}
|
||||
_.memory.Free(damageTypeArray);
|
||||
}
|
||||
|
||||
protected function DefaultIt()
|
||||
{
|
||||
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'
|
||||
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'
|
||||
}
|
||||
186
kf_sources/AcediaFixes/Classes/FixFFHack_Feature.uc
Normal file
186
kf_sources/AcediaFixes/Classes/FixFFHack_Feature.uc
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<DamageType> > alwaysScale;
|
||||
// Damage types, for which we should never reapply friendly fire scaling.
|
||||
var private /*config*/ array< class<DamageType> > neverScale;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
_server.unreal.gameRules.OnNetDamage(self).connect = NetDamage;
|
||||
}
|
||||
|
||||
protected function OnDisabled()
|
||||
{
|
||||
_server.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> 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 *= _server.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> damageType)
|
||||
{
|
||||
local int i;
|
||||
local array< class<DamageType> > 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'
|
||||
}
|
||||
50
kf_sources/AcediaFixes/Classes/FixInfiniteNades.uc
Normal file
50
kf_sources/AcediaFixes/Classes/FixInfiniteNades.uc
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Config object for `FixInfiniteNades_Feature`.
|
||||
* Copyright 2021-2022 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixInfiniteNades extends FeatureConfig
|
||||
perobjectconfig
|
||||
config(AcediaFixes);
|
||||
|
||||
var public config bool ignoreTossFlags;
|
||||
|
||||
protected function HashTable ToData()
|
||||
{
|
||||
local HashTable data;
|
||||
data = __().collections.EmptyHashTable();
|
||||
data.SetBool(P("ignoreTossFlags"), ignoreTossFlags);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected function FromData(HashTable source)
|
||||
{
|
||||
if (source != none) {
|
||||
ignoreTossFlags = source.GetBool(P("ignoreTossFlags"), true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function DefaultIt()
|
||||
{
|
||||
ignoreTossFlags = true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaFixes"
|
||||
ignoreTossFlags = true
|
||||
}
|
||||
273
kf_sources/AcediaFixes/Classes/FixInfiniteNades_Feature.uc
Normal file
273
kf_sources/AcediaFixes/Classes/FixInfiniteNades_Feature.uc
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<FragAmmoRecord> ammoRecords;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
local Frag nextFrag;
|
||||
local LevelInfo level;
|
||||
level = _server.unreal.GetLevel();
|
||||
_server.unreal.OnTick(self).connect = Tick;
|
||||
_server.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()
|
||||
{
|
||||
_server.unreal.OnTick(self).Disconnect();
|
||||
_server.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<FragAmmoRecord> 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 = _server.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
|
||||
}
|
||||
114
kf_sources/AcediaFixes/Classes/FixInventoryAbuse.uc
Normal file
114
kf_sources/AcediaFixes/Classes/FixInventoryAbuse.uc
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* Config object for `FixInventoryAbuse_Feature`.
|
||||
* Copyright 2021-2022 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixInventoryAbuse extends FeatureConfig
|
||||
perobjectconfig
|
||||
config(AcediaFixes)
|
||||
dependson(FixInventoryAbuse_Feature);
|
||||
|
||||
var public config float checkInterval;
|
||||
var public config array<FixInventoryAbuse_Feature.DualiesPair> dualiesClasses;
|
||||
|
||||
protected function HashTable ToData()
|
||||
{
|
||||
local int i;
|
||||
local ArrayList pairsArray;
|
||||
local HashTable data, pair;
|
||||
data = _.collections.EmptyHashTable();
|
||||
data.SetFloat(P("checkInterval"), checkInterval);
|
||||
pairsArray = _.collections.EmptyArrayList();
|
||||
for (i = 0; i < dualiesClasses.length; i += 1)
|
||||
{
|
||||
pair = _.collections.EmptyHashTable();
|
||||
pair.SetItem( P("single"),
|
||||
_.text.FromString(string(dualiesClasses[i].single)));
|
||||
pair.SetItem( P("dual"),
|
||||
_.text.FromString(string(dualiesClasses[i].dual)));
|
||||
pairsArray.AddItem(pair);
|
||||
}
|
||||
data.SetItem(P("dualiesClasses"), pairsArray);
|
||||
_.memory.Free(pairsArray);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected function FromData(HashTable source)
|
||||
{
|
||||
local int i;
|
||||
local ArrayList pairsArray;
|
||||
local HashTable loadedPair;
|
||||
local FixInventoryAbuse_Feature.DualiesPair newPair;
|
||||
if (source == none) {
|
||||
return;
|
||||
}
|
||||
checkInterval = source.GetFloat(P("checkInterval"), 0.25);
|
||||
pairsArray = source.GetArrayList(P("dualiesClasses"));
|
||||
dualiesClasses.length = 0;
|
||||
if (pairsArray == none) {
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < pairsArray.GetLength(); i += 1)
|
||||
{
|
||||
loadedPair = pairsArray.GetHashTable(i);
|
||||
if (loadedPair == none) continue;
|
||||
|
||||
newPair.single = class<KFWeaponPickup>(
|
||||
_.memory.LoadClass(loadedPair.GetText(P("single"))) );
|
||||
newPair.dual = class<KFWeaponPickup>(
|
||||
_.memory.LoadClass(loadedPair.GetText(P("dual"))) );
|
||||
dualiesClasses[dualiesClasses.length] = newPair;
|
||||
}
|
||||
_.memory.Free(pairsArray);
|
||||
}
|
||||
|
||||
protected function DefaultIt()
|
||||
{
|
||||
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')
|
||||
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')
|
||||
}
|
||||
237
kf_sources/AcediaFixes/Classes/FixInventoryAbuse_Feature.uc
Normal file
237
kf_sources/AcediaFixes/Classes/FixInventoryAbuse_Feature.uc
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<KFWeaponPickup> single;
|
||||
var class<KFWeaponPickup> 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<DualiesPair> dualiesClasses;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
local float actualInterval;
|
||||
actualInterval = checkInterval;
|
||||
if (actualInterval <= 0) {
|
||||
actualInterval = 0.25;
|
||||
}
|
||||
checkTimer = _server.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<KFWeaponPickup> GetRootPickupClass(KFWeapon weapon)
|
||||
{
|
||||
local int i;
|
||||
local class<KFWeaponPickup> root;
|
||||
if (weapon == none) return none;
|
||||
// Start with a pickup of the given weapons
|
||||
root = class<KFWeaponPickup>(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<KFWeaponPickup>(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<KFWeaponPickup> rootClass;
|
||||
local array< class<Pickup> > 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<KFWeapon> 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<ConnectionService.Connection> 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')
|
||||
}
|
||||
56
kf_sources/AcediaFixes/Classes/FixLogSpam.uc
Normal file
56
kf_sources/AcediaFixes/Classes/FixLogSpam.uc
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* Config object for `FixLogSpam_Feature`.
|
||||
* Copyright 2021-2022 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixLogSpam extends FeatureConfig
|
||||
perobjectconfig
|
||||
config(AcediaFixes);
|
||||
|
||||
var public config bool fixPickupSpam;
|
||||
var public config bool fixTraderSpam;
|
||||
|
||||
protected function HashTable ToData()
|
||||
{
|
||||
local HashTable data;
|
||||
data = __().collections.EmptyHashTable();
|
||||
data.SetBool(P("fixPickupSpam"), fixPickupSpam);
|
||||
data.SetBool(P("fixTraderSpam"), fixTraderSpam);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected function FromData(HashTable source)
|
||||
{
|
||||
if (source != none)
|
||||
{
|
||||
fixPickupSpam = source.GetBool(P("fixPickupSpam"), true);
|
||||
fixTraderSpam = source.GetBool(P("fixTraderSpam"), true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function DefaultIt()
|
||||
{
|
||||
fixPickupSpam = true;
|
||||
fixTraderSpam = true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaFixes"
|
||||
fixPickupSpam = true
|
||||
fixTraderSpam = true
|
||||
}
|
||||
93
kf_sources/AcediaFixes/Classes/FixLogSpam_Feature.uc
Normal file
93
kf_sources/AcediaFixes/Classes/FixLogSpam_Feature.uc
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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
|
||||
}
|
||||
71
kf_sources/AcediaFixes/Classes/FixPipes.uc
Normal file
71
kf_sources/AcediaFixes/Classes/FixPipes.uc
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Config object for `FixPipes_Feature`.
|
||||
* Copyright 2021-2022 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixPipes extends FeatureConfig
|
||||
perobjectconfig
|
||||
config(AcediaFixes);
|
||||
|
||||
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 HashTable ToData()
|
||||
{
|
||||
local HashTable data;
|
||||
data = __().collections.EmptyHashTable();
|
||||
data.SetBool(P("preventMassiveDamage"), preventMassiveDamage);
|
||||
data.SetBool(P("preventSuspiciousDamage"), preventSuspiciousDamage);
|
||||
data.SetBool(P("preventCorpseDetonation"), preventCorpseDetonation);
|
||||
data.SetBool(P("preventNPCDetonation"), preventNPCDetonation);
|
||||
data.SetFloat(P("proximityCheckElevation"), proximityCheckElevation);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected function FromData(HashTable source)
|
||||
{
|
||||
if (source == none) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
protected function DefaultIt()
|
||||
{
|
||||
preventMassiveDamage = true;
|
||||
preventSuspiciousDamage = true;
|
||||
preventCorpseDetonation = true;
|
||||
preventNPCDetonation = true;
|
||||
proximityCheckElevation = 20.0;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaFixes"
|
||||
preventMassiveDamage = true
|
||||
preventSuspiciousDamage = true
|
||||
preventCorpseDetonation = true
|
||||
preventNPCDetonation = true
|
||||
proximityCheckElevation = 20.0
|
||||
}
|
||||
480
kf_sources/AcediaFixes/Classes/FixPipes_Feature.uc
Normal file
480
kf_sources/AcediaFixes/Classes/FixPipes_Feature.uc
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<PipeRecord> 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;
|
||||
_server.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement;
|
||||
// Set cleanup timer, there is little point to making
|
||||
// clean up interval configurable.
|
||||
cleanupTimer = _server.time.StartTimer(5.0, true);
|
||||
cleanupTimer.OnElapsed(self).connect = CleanPipeRecords;
|
||||
// Fix pipes that are already lying about on the map
|
||||
level = _server.unreal.GetLevel();
|
||||
foreach level.DynamicActors(class'KFMod.PipeBombProjectile', nextPipe) {
|
||||
RegisterPipe(nextPipe);
|
||||
}
|
||||
}
|
||||
|
||||
protected function OnDisabled()
|
||||
{
|
||||
local int i;
|
||||
class'PipeBombProjectile'.default.bGameRelevant = pipesRelevancyFlag;
|
||||
_server.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 = _server.unreal.ActorRef(newPipe);
|
||||
// Setup `PipesSafetyCollision` for catching `TakeDamage()` events
|
||||
// (only if we need to according to settings)
|
||||
if (NeedSafetyCollision())
|
||||
{
|
||||
newRecord.safetyCollision = _server.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
|
||||
}
|
||||
50
kf_sources/AcediaFixes/Classes/FixProjectileFF.uc
Normal file
50
kf_sources/AcediaFixes/Classes/FixProjectileFF.uc
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Config object for `FixProjectileFF_Feature`.
|
||||
* Copyright 2021-2022 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFF extends FeatureConfig
|
||||
perobjectconfig
|
||||
config(AcediaFixes);
|
||||
|
||||
var public config bool ignoreFriendlyFire;
|
||||
|
||||
protected function HashTable ToData()
|
||||
{
|
||||
local HashTable data;
|
||||
data = __().collections.EmptyHashTable();
|
||||
data.SetBool(P("ignoreFriendlyFire"), ignoreFriendlyFire);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected function FromData(HashTable source)
|
||||
{
|
||||
if (source != none) {
|
||||
ignoreFriendlyFire = source.GetBool(P("ignoreFriendlyFire"));
|
||||
}
|
||||
}
|
||||
|
||||
protected function DefaultIt()
|
||||
{
|
||||
ignoreFriendlyFire = false;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaFixes"
|
||||
ignoreFriendlyFire = false
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* A helper class for `FixProjectileFF` that adds an instigator and
|
||||
* friendly fire checks to `TakeDamage()` method to avoid exploding projectiles
|
||||
* when not expected.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFFClass_FlareRevolverProjectile
|
||||
extends FlareRevolverProjectile;
|
||||
|
||||
function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigatedBy,
|
||||
Vector hitLocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local bool canTakeThisDamage;
|
||||
if (class<SirenScreamDamage>(damageType) != none)
|
||||
{
|
||||
Disintegrate(hitLocation, Vect(0, 0, 1));
|
||||
return;
|
||||
}
|
||||
canTakeThisDamage =
|
||||
(instigatedBy == instigator)
|
||||
|| class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
|
||||
if (canTakeThisDamage && !bDud) {
|
||||
Explode(hitLocation, Vect(0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
RemoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* A helper class for `FixProjectileFF` that adds an instigator and
|
||||
* friendly fire checks to `TakeDamage()` method to avoid exploding projectiles
|
||||
* when not expected.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFFClass_HuskGunProjectile extends HuskGunProjectile;
|
||||
|
||||
function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigatedBy,
|
||||
Vector hitLocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local bool canTakeThisDamage;
|
||||
if (class<SirenScreamDamage>(damageType) != none)
|
||||
{
|
||||
Disintegrate(hitLocation, Vect(0, 0, 1));
|
||||
return;
|
||||
}
|
||||
canTakeThisDamage =
|
||||
(instigatedBy == instigator)
|
||||
|| class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
|
||||
if (canTakeThisDamage && !bDud) {
|
||||
Explode(hitLocation, Vect(0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
RemoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* A helper class for `FixProjectileFF` that adds an instigator and
|
||||
* friendly fire checks to `TakeDamage()` method to avoid exploding projectiles
|
||||
* when not expected.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFFClass_LAWProj extends LAWProj;
|
||||
|
||||
function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigatedBy,
|
||||
Vector hitLocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local bool canTakeThisDamage;
|
||||
if (class<SirenScreamDamage>(damageType) != none)
|
||||
{
|
||||
Disintegrate(hitLocation, Vect(0, 0, 1));
|
||||
return;
|
||||
}
|
||||
canTakeThisDamage =
|
||||
(instigatedBy == instigator)
|
||||
|| class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
|
||||
if (canTakeThisDamage && !bDud) {
|
||||
Explode(hitLocation, Vect(0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
RemoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* A helper class for `FixProjectileFF` that adds an instigator and
|
||||
* friendly fire checks to `TakeDamage()` method to avoid exploding projectiles
|
||||
* when not expected.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFFClass_M203GrenadeProjectile extends M203GrenadeProjectile;
|
||||
|
||||
function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigatedBy,
|
||||
Vector hitLocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local bool canTakeThisDamage;
|
||||
if (class<SirenScreamDamage>(damageType) != none)
|
||||
{
|
||||
Disintegrate(hitLocation, Vect(0, 0, 1));
|
||||
return;
|
||||
}
|
||||
canTakeThisDamage =
|
||||
(instigatedBy == instigator)
|
||||
|| class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
|
||||
if (canTakeThisDamage && !bDud) {
|
||||
Explode(hitLocation, Vect(0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
RemoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* A helper class for `FixProjectileFF` that adds an instigator and
|
||||
* friendly fire checks to `TakeDamage()` method to avoid exploding projectiles
|
||||
* when not expected.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFFClass_M32GrenadeProjectile extends M32GrenadeProjectile;
|
||||
|
||||
function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigatedBy,
|
||||
Vector hitLocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local bool canTakeThisDamage;
|
||||
if (class<SirenScreamDamage>(damageType) != none)
|
||||
{
|
||||
Disintegrate(hitLocation, Vect(0, 0, 1));
|
||||
return;
|
||||
}
|
||||
canTakeThisDamage =
|
||||
(instigatedBy == instigator)
|
||||
|| class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
|
||||
if (canTakeThisDamage && !bDud) {
|
||||
Explode(hitLocation, Vect(0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
RemoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* A helper class for `FixProjectileFF` that adds an instigator and
|
||||
* friendly fire checks to `TakeDamage()` method to avoid exploding projectiles
|
||||
* when not expected.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFFClass_M79GrenadeProjectile extends M79GrenadeProjectile;
|
||||
|
||||
function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigatedBy,
|
||||
Vector hitLocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local bool canTakeThisDamage;
|
||||
if (class<SirenScreamDamage>(damageType) != none)
|
||||
{
|
||||
Disintegrate(hitLocation, Vect(0, 0, 1));
|
||||
return;
|
||||
}
|
||||
canTakeThisDamage =
|
||||
(instigatedBy == instigator)
|
||||
|| class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
|
||||
if (canTakeThisDamage && !bDud) {
|
||||
Explode(hitLocation, Vect(0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
RemoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* A helper class for `FixProjectileFF` that adds an instigator and
|
||||
* friendly fire checks to `TakeDamage()` method to avoid exploding projectiles
|
||||
* when not expected.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFFClass_SPGrenadeProjectile extends SPGrenadeProjectile;
|
||||
|
||||
var private SPGrenadeProjectile projectileFace;
|
||||
|
||||
public final function SetFace(SPGrenadeProjectile newProjectileFace)
|
||||
{
|
||||
projectileFace = newProjectileFace;
|
||||
}
|
||||
|
||||
public function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigatedBy,
|
||||
Vector hitLocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local bool canTakeThisDamage;
|
||||
if (class<SirenScreamDamage>(damageType) != none)
|
||||
{
|
||||
Disintegrate(hitLocation, Vect(0, 0, 1));
|
||||
return;
|
||||
}
|
||||
canTakeThisDamage =
|
||||
(instigatedBy == instigator)
|
||||
|| class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
|
||||
// Unlike M79/M32 - no `!bDud` check, since it's supposed to fall down
|
||||
if (canTakeThisDamage)
|
||||
{
|
||||
Explode(hitLocation, Vect(0, 0, 0));
|
||||
if (projectileFace != none) {
|
||||
projectileFace.Explode(hitLocation, Vect(0, 0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
simulated function Explode(vector hitLocation, vector HitNormal)
|
||||
{
|
||||
super.Explode(hitLocation, hitNormal);
|
||||
if (projectileFace != none) {
|
||||
projectileFace.Explode(hitLocation, hitNormal);
|
||||
}
|
||||
}
|
||||
|
||||
simulated function Disintegrate(vector hitLocation, vector hitNormal)
|
||||
{
|
||||
super.Disintegrate(hitLocation, hitNormal);
|
||||
if (projectileFace != none) {
|
||||
projectileFace.Disintegrate(hitLocation, hitNormal);
|
||||
}
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
super.Tick(delta);
|
||||
if (projectileFace != none)
|
||||
{
|
||||
projectileFace.SetLocation(location);
|
||||
projectileFace.SetRotation(rotation);
|
||||
projectileFace.velocity = velocity;
|
||||
}
|
||||
}
|
||||
|
||||
event OnDestroyed()
|
||||
{
|
||||
if (projectileFace != none) {
|
||||
projectileFace.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
RemoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* A helper class for `FixProjectileFF` that adds an instigator and
|
||||
* friendly fire checks to `TakeDamage()` method to avoid exploding projectiles
|
||||
* when not expected.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFFClass_SealSquealProjectile extends SealSquealProjectile;
|
||||
|
||||
var private SealSquealProjectile projectileFace;
|
||||
|
||||
public final function SetFace(SealSquealProjectile newProjectileFace)
|
||||
{
|
||||
projectileFace = newProjectileFace;
|
||||
}
|
||||
|
||||
public function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigatedBy,
|
||||
Vector hitLocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local bool canTakeThisDamage;
|
||||
if (class<SirenScreamDamage>(damageType) != none)
|
||||
{
|
||||
Disintegrate(hitLocation, Vect(0, 0, 1));
|
||||
return;
|
||||
}
|
||||
canTakeThisDamage =
|
||||
(instigatedBy == instigator)
|
||||
|| class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
|
||||
// Unlike M79/M32 - no `!bDud` check, since it's supposed to fall down
|
||||
if (canTakeThisDamage)
|
||||
{
|
||||
Explode(hitLocation, Vect(0, 0, 0));
|
||||
if (projectileFace != none) {
|
||||
projectileFace.Explode(hitLocation, Vect(0, 0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
simulated function Explode(vector hitLocation, vector HitNormal)
|
||||
{
|
||||
super.Explode(hitLocation, hitNormal);
|
||||
if (projectileFace != none) {
|
||||
projectileFace.Explode(hitLocation, hitNormal);
|
||||
}
|
||||
}
|
||||
|
||||
simulated function Disintegrate(vector hitLocation, vector hitNormal)
|
||||
{
|
||||
super.Disintegrate(hitLocation, hitNormal);
|
||||
if (projectileFace != none) {
|
||||
projectileFace.Disintegrate(hitLocation, hitNormal);
|
||||
}
|
||||
}
|
||||
|
||||
simulated function Stick(Actor hitActor, vector hitLocation)
|
||||
{
|
||||
super.Stick(hitActor, hitLocation);
|
||||
if (projectileFace != none)
|
||||
{
|
||||
projectileFace.SetCollision(true, true);
|
||||
projectileFace.SetLocation(location);
|
||||
projectileFace.SetRotation(rotation);
|
||||
projectileFace.Stick(hitActor, hitLocation);
|
||||
}
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
super.Tick(delta);
|
||||
if (projectileFace == none) return;
|
||||
if (projectileFace.bStuck) return;
|
||||
|
||||
projectileFace.SetLocation(location);
|
||||
projectileFace.SetRotation(rotation);
|
||||
projectileFace.velocity = velocity;
|
||||
}
|
||||
|
||||
event OnDestroyed()
|
||||
{
|
||||
if (projectileFace != none) {
|
||||
projectileFace.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
RemoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* A helper class for `FixProjectileFF` that adds an instigator and
|
||||
* friendly fire checks to `TakeDamage()` method to avoid exploding projectiles
|
||||
* when not expected.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFFClass_ZEDGunProjectile extends ZEDGunProjectile;
|
||||
|
||||
function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigatedBy,
|
||||
Vector hitLocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local bool canTakeThisDamage;
|
||||
if (class<SirenScreamDamage>(damageType) != none)
|
||||
{
|
||||
Disintegrate(hitLocation, Vect(0, 0, 1));
|
||||
return;
|
||||
}
|
||||
canTakeThisDamage =
|
||||
(instigatedBy == instigator)
|
||||
|| class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
|
||||
if (canTakeThisDamage && !bDud) {
|
||||
Explode(hitLocation, Vect(0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
RemoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* A helper class for `FixProjectileFF` that adds an instigator and
|
||||
* friendly fire checks to `TakeDamage()` method to avoid exploding projectiles
|
||||
* when not expected.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFFClass_ZEDMKIIPrimaryProjectile
|
||||
extends ZEDMKIIPrimaryProjectile;
|
||||
|
||||
function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigatedBy,
|
||||
Vector hitLocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local bool canTakeThisDamage;
|
||||
if (class<SirenScreamDamage>(damageType) != none)
|
||||
{
|
||||
Disintegrate(hitLocation, Vect(0, 0, 1));
|
||||
return;
|
||||
}
|
||||
canTakeThisDamage =
|
||||
(instigatedBy == instigator)
|
||||
|| class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
|
||||
if (canTakeThisDamage && !bDud) {
|
||||
Explode(hitLocation, Vect(0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
RemoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* A helper class for `FixProjectileFF` that adds an instigator and
|
||||
* friendly fire checks to `TakeDamage()` method to avoid exploding projectiles
|
||||
* when not expected.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixProjectileFFClass_ZEDMKIISecondaryProjectile
|
||||
extends ZEDMKIISecondaryProjectile;
|
||||
|
||||
function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigatedBy,
|
||||
Vector hitLocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local bool canTakeThisDamage;
|
||||
if (class<SirenScreamDamage>(damageType) != none)
|
||||
{
|
||||
Disintegrate(hitLocation, Vect(0, 0, 1));
|
||||
return;
|
||||
}
|
||||
canTakeThisDamage =
|
||||
(instigatedBy == instigator)
|
||||
|| class'FixProjectileFF_Feature'.static.IsFriendlyFireAcceptable();
|
||||
if (canTakeThisDamage && !bDud) {
|
||||
Explode(hitLocation, Vect(0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
RemoteRole = ROLE_None
|
||||
}
|
||||
312
kf_sources/AcediaFixes/Classes/FixProjectileFF_Feature.uc
Normal file
312
kf_sources/AcediaFixes/Classes/FixProjectileFF_Feature.uc
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<ROBallisticProjectile> vulnerableClass;
|
||||
var class<ROBallisticProjectile> protectedClass;
|
||||
};
|
||||
var private const array<ReplacementRule> 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;
|
||||
}
|
||||
_server.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;
|
||||
}
|
||||
_server.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<ROBallisticProjectile> 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<ROBallisticProjectile> 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<ROBallisticProjectile> FindFixedClass(
|
||||
class<ROBallisticProjectile> projectileClass)
|
||||
{
|
||||
local int i;
|
||||
local array<ReplacementRule> 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 __server().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')
|
||||
}
|
||||
56
kf_sources/AcediaFixes/Classes/FixSpectatorCrash.uc
Normal file
56
kf_sources/AcediaFixes/Classes/FixSpectatorCrash.uc
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* Config object for `FixSpectatorCrash_Feature`.
|
||||
* Copyright 2021-2022 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixSpectatorCrash extends FeatureConfig
|
||||
perobjectconfig
|
||||
config(AcediaFixes);
|
||||
|
||||
var public config float spectatorChangeTimeout;
|
||||
var public config bool allowServerBlock;
|
||||
|
||||
protected function HashTable ToData()
|
||||
{
|
||||
local HashTable data;
|
||||
data = __().collections.EmptyHashTable();
|
||||
data.SetBool(P("allowServerBlock"), allowServerBlock);
|
||||
data.SetFloat(P("spectatorChangeTimeout"), spectatorChangeTimeout);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected function FromData(HashTable source)
|
||||
{
|
||||
if (source == none) {
|
||||
return;
|
||||
}
|
||||
allowServerBlock = source.GetBool(P("allowServerBlock"), true);
|
||||
spectatorChangeTimeout = source.GetFloat(P("spectatorChangeTimeout"), 0.25);
|
||||
}
|
||||
|
||||
protected function DefaultIt()
|
||||
{
|
||||
spectatorChangeTimeout = 0.25;
|
||||
allowServerBlock = true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaFixes"
|
||||
spectatorChangeTimeout = 0.25
|
||||
allowServerBlock = true
|
||||
}
|
||||
341
kf_sources/AcediaFixes/Classes/FixSpectatorCrash_Feature.uc
Normal file
341
kf_sources/AcediaFixes/Classes/FixSpectatorCrash_Feature.uc
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<CooldownRecord> 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<NativeActorRef> 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()
|
||||
{
|
||||
_server.unreal.OnTick(self).connect = Tick;
|
||||
_server.unreal.broadcasts.OnHandleLocalized(self).connect = HandleLocalized;
|
||||
}
|
||||
|
||||
protected function OnDisabled()
|
||||
{
|
||||
_server.unreal.OnTick(self).Disconnect();
|
||||
_server.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 = _server.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 = _server.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<ConnectionService.Connection> 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 _server.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
|
||||
}
|
||||
57
kf_sources/AcediaFixes/Classes/FixZedTimeLags.uc
Normal file
57
kf_sources/AcediaFixes/Classes/FixZedTimeLags.uc
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Config object for `FixZedTimeLags_Feature`.
|
||||
* Copyright 2021-2022 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixZedTimeLags extends FeatureConfig
|
||||
perobjectconfig
|
||||
config(AcediaFixes);
|
||||
|
||||
var public config int maxGameSpeedUpdatesAmount;
|
||||
var public config bool disableTick;
|
||||
|
||||
protected function HashTable ToData()
|
||||
{
|
||||
local HashTable data;
|
||||
data = __().collections.EmptyHashTable();
|
||||
data.SetBool(P("disableTick"), disableTick);
|
||||
data.SetInt(P("maxGameSpeedUpdatesAmount"), maxGameSpeedUpdatesAmount);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected function FromData(HashTable source)
|
||||
{
|
||||
if (source == none) {
|
||||
return;
|
||||
}
|
||||
disableTick = source.GetBool(P("disableTick"), true);
|
||||
maxGameSpeedUpdatesAmount =
|
||||
source.GetInt(P("maxGameSpeedUpdatesAmount"), 3);
|
||||
}
|
||||
|
||||
protected function DefaultIt()
|
||||
{
|
||||
maxGameSpeedUpdatesAmount = 3;
|
||||
disableTick = true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaFixes"
|
||||
maxGameSpeedUpdatesAmount = 3
|
||||
disableTick = true
|
||||
}
|
||||
191
kf_sources/AcediaFixes/Classes/FixZedTimeLags_Feature.uc
Normal file
191
kf_sources/AcediaFixes/Classes/FixZedTimeLags_Feature.uc
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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) {
|
||||
_server.unreal.GetGameType().Disable('Tick');
|
||||
}
|
||||
_server.unreal.OnTick(self).connect = Tick;
|
||||
}
|
||||
|
||||
protected function OnDisabled()
|
||||
{
|
||||
if (disableTick) {
|
||||
_server.unreal.GetGameType().Enable('Tick');
|
||||
}
|
||||
_server.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 = _server.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<ConnectionService.Connection> 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
|
||||
}
|
||||
36
kf_sources/AcediaFixes/Classes/FixedFragFire.uc
Normal file
36
kf_sources/AcediaFixes/Classes/FixedFragFire.uc
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* A replacement for vanilla `FragFire` fire class for `Frag` weapon that
|
||||
* adds additional ammo check in accordance to ammo records
|
||||
* of `FixInfiniteNades`.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class FixedFragFire extends KFMod.FragFire;
|
||||
|
||||
function DoFireEffect()
|
||||
{
|
||||
local FixInfiniteNades_Feature nadeFix;
|
||||
nadeFix = FixInfiniteNades_Feature(
|
||||
class'FixInfiniteNades_Feature'.static.GetEnabledInstance());
|
||||
if (nadeFix == none || nadeFix.RegisterNadeThrow(Frag(weapon))) {
|
||||
super.DoFireEffect();
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
251
kf_sources/AcediaFixes/Classes/HelperPickup.uc
Normal file
251
kf_sources/AcediaFixes/Classes/HelperPickup.uc
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
/**
|
||||
* Helper object for `FixLogSpam` fix, responsible for fixing log spam
|
||||
* due to picking up dropped weapons without set `inventory` variable.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class HelperPickup extends AcediaObject;
|
||||
|
||||
/**
|
||||
* `KFWeaponPickup` class is responsible for spamming log with
|
||||
* error messages about missing `inventory` actor: "Accessed None 'Inventory'".
|
||||
* This is caused by `Destroyed()` event that tries to call `KFGameType`'s
|
||||
* `WeaponDestroyed()` on pickup's `inventory.class`, but it fails because
|
||||
* `inventory` is essentially guaranteed to be `none` for pickups dropped
|
||||
* by players. Since no `inventory != none` check is made, this leads to
|
||||
* log message spam.
|
||||
* To fix it we simply disable `Destroyed()` event altogether, since all
|
||||
* it does is:
|
||||
* 1. Fails as we have described;
|
||||
* 2. In `super(Pickup).Destroyed()` sets `myMarker.markedItem` to `none`,
|
||||
* which does nothig for pickups dropped by players, since they do not
|
||||
* have any `myMarker` in the first place: they are usually defined to
|
||||
* refer to `InventorySpot`s, generated for placed pickups when
|
||||
* navigation paths are built.
|
||||
*
|
||||
* The are two issues that need resolving for this solution to work:
|
||||
* 1. Distinguish between pickups dropped by the player versus pickups
|
||||
* placed on the map;
|
||||
* 2. Even if one disables `Destroyed()` event, it can get re-enabled again
|
||||
* (possibly by a state change).
|
||||
* One way to accomplish the first goal is to check `instigator` during
|
||||
* `CheckReplacement()` event: it should be `none` for map pickups and point
|
||||
* to the player's pawn for dropped pickups. If, for some reason, instigator
|
||||
* would end up being equal to `none` for a dropped pickup, we also record all
|
||||
* the other pickups and give them some time to initialize
|
||||
* (`CheckReplacement()` is called at the moment they are spawned and
|
||||
* before initialization logic is completed) and check their `bDropped` flag,
|
||||
* since it is what decides whether log message about accessing `none`-value
|
||||
* will be output: `if ( bDropped && class<Weapon>(Inventory.Class) != none )`.
|
||||
* Then we also try to fix all the weapons with `bDropped == true`.
|
||||
* To resolve issue with `Destroyed()` event being re-enabled we use
|
||||
* brute force approach of disabling it every tick:
|
||||
* 1. Amount of dropped pickups is small enough for it to not cause
|
||||
* performance issues;
|
||||
* 2. Checking change in state or trying to determine other causes for
|
||||
* re-enabling the event would likely just lead to more complicated
|
||||
* logic with around the same or even worse performance.
|
||||
* We also enforce additional update when we catch player trying to pickup some
|
||||
* `KFWeaponPickup` with `GameRules`'s `OverridePickupQuery()`, which is
|
||||
* called soon before pickup's destruction. Note that this update cannot
|
||||
* replace update inside the `Tick()`, since pickup can be destroyed without
|
||||
* being picked up, i.e. at the start of each wave.
|
||||
*
|
||||
* Described method does not 100% guarantee removal of the related spam
|
||||
* messages, but should make them virtually impossible. At least on
|
||||
* vanilla servers.
|
||||
*/
|
||||
|
||||
// For easy access in relevant `GameRules` and mutator event listener.
|
||||
var private HelperPickup singletonInstance;
|
||||
// Already fixed pickups that we periodically "refix".
|
||||
var private array<NativeActorRef> recordedPickups;
|
||||
// Pickups that will get `bDropped` flag checked the next tick to
|
||||
// determine if we need to fix them.
|
||||
var private array<NativeActorRef> pendingPickups;
|
||||
|
||||
protected function Constructor()
|
||||
{
|
||||
local LevelInfo level;
|
||||
local KFWeaponPickup nextPickup;
|
||||
if (default.singletonInstance == none) {
|
||||
default.singletonInstance = self;
|
||||
}
|
||||
// To detect when player tries to pick something up
|
||||
// (and force additional pickup fix update)
|
||||
_server.unreal.gameRules.OnOverridePickupQuery(self).connect = PickupQuery;
|
||||
// To detect newly spawned pickups
|
||||
_server.unreal.mutator.OnCheckReplacement(self).connect = CheckReplacement;
|
||||
// For updating pickups as soon as possible
|
||||
_server.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`
|
||||
// was created during one's initialization. This will give it time to
|
||||
// set up variables that distinguish dropped pickup from another
|
||||
// kind of pickup.
|
||||
level = _server.unreal.GetLevel();
|
||||
foreach level.DynamicActors(class'KFMod.KFWeaponPickup', nextPickup)
|
||||
{
|
||||
pendingPickups[pendingPickups.length] =
|
||||
_server.unreal.ActorRef(nextPickup);
|
||||
}
|
||||
}
|
||||
|
||||
protected function Finalizer()
|
||||
{
|
||||
local int i;
|
||||
local KFWeaponPickup nextPickup;
|
||||
for (i = 0; i < recordedPickups.length; i += 1)
|
||||
{
|
||||
nextPickup = KFWeaponPickup(recordedPickups[i].Get());
|
||||
if (nextPickup != none) {
|
||||
recordedPickups[i].Enable('Destroyed');
|
||||
}
|
||||
}
|
||||
_.memory.FreeMany(recordedPickups);
|
||||
_.memory.FreeMany(pendingPickups);
|
||||
recordedPickups.length = 0;
|
||||
pendingPickups.length = 0;
|
||||
_server.unreal.gameRules.OnOverridePickupQuery(self).Disconnect();
|
||||
_server.unreal.mutator.OnCheckReplacement(self).Disconnect();
|
||||
_server.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()
|
||||
{
|
||||
return default.singletonInstance;
|
||||
}
|
||||
|
||||
// Checks whether given pickup is recorded in any of our records,
|
||||
// including pending pickups.
|
||||
// `none` is never recorded.
|
||||
private final function bool IsPickupRecorded(KFWeaponPickup pickupToCheck)
|
||||
{
|
||||
local int i;
|
||||
local KFWeaponPickup nextPickup;
|
||||
if (pickupToCheck == none) {
|
||||
return false;
|
||||
}
|
||||
for (i = 0; i < recordedPickups.length; i += 1)
|
||||
{
|
||||
nextPickup = KFWeaponPickup(recordedPickups[i].Get());
|
||||
if (nextPickup == pickupToCheck) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < pendingPickups.length; i += 1)
|
||||
{
|
||||
nextPickup = KFWeaponPickup(pendingPickups[i].Get());
|
||||
if (nextPickup == pickupToCheck) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gets rid of pickups that got destroyed at some point and now are set
|
||||
// to `none`.
|
||||
private final function CleanRecordedPickups()
|
||||
{
|
||||
local int i;
|
||||
while (i < recordedPickups.length)
|
||||
{
|
||||
if (recordedPickups[i].Get() == none)
|
||||
{
|
||||
recordedPickups[i].FreeSelf();
|
||||
recordedPickups.Remove(i, 1);
|
||||
}
|
||||
else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Makes helper handle given pickup `newPickup` by either fixing it or
|
||||
// delaying to check whether it is dropped.
|
||||
public final function HandlePickup(KFWeaponPickup newPickup)
|
||||
{
|
||||
if (newPickup == none) return;
|
||||
if (IsPickupRecorded(newPickup)) return;
|
||||
|
||||
if (newPickup.instigator != none)
|
||||
{
|
||||
newPickup.Disable('Destroyed');
|
||||
recordedPickups[recordedPickups.length] =
|
||||
_server.unreal.ActorRef(newPickup);
|
||||
}
|
||||
else
|
||||
{
|
||||
pendingPickups[pendingPickups.length] =
|
||||
_server.unreal.ActorRef(newPickup);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-disables `Destroyed()` event for all recorded (confirmed) dropped pickups
|
||||
// and processes all pending (for the check if they are dropped) pickups.
|
||||
private final function UpdatePickups()
|
||||
{
|
||||
local int i;
|
||||
local KFWeaponPickup nextPickup;
|
||||
for (i = 0; i < recordedPickups.length; i += 1)
|
||||
{
|
||||
nextPickup = KFWeaponPickup(recordedPickups[i].Get());
|
||||
if (nextPickup != none) {
|
||||
nextPickup.Disable('Destroyed');
|
||||
}
|
||||
}
|
||||
for (i = 0; i < pendingPickups.length; i += 1)
|
||||
{
|
||||
nextPickup = KFWeaponPickup(pendingPickups[i].Get());
|
||||
if (nextPickup == none) continue;
|
||||
if (nextPickup.bDropped) continue;
|
||||
|
||||
nextPickup.Disable('Destroyed');
|
||||
recordedPickups[recordedPickups.length] = pendingPickups[i];
|
||||
}
|
||||
pendingPickups.length = 0;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
157
kf_sources/AcediaFixes/Classes/HelperTrader.uc
Normal file
157
kf_sources/AcediaFixes/Classes/HelperTrader.uc
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/**
|
||||
* Helper object for `FixLogSpam` fix, responsible for fixing log spam
|
||||
* due to missing `ShopVolume`s missing `WeaponLocker` in the `myTrader`
|
||||
* variable.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class HelperTrader extends AcediaObject
|
||||
config(AcediaFixes);
|
||||
|
||||
/**
|
||||
* `WeaponLocker` seems be an actor that was supposed to play a certain
|
||||
* animation whenever players enter a shop. Each `WeaponLocker` is also
|
||||
* supposed to find the shop (`ShopVolume`) it's linked to during
|
||||
* `PreBeginPlay()` event and record itself inside `ShopVolume`'s `myTrader`
|
||||
* variable. However this sometimes does not happen: either due to locker's
|
||||
* placement or due to map maker forgetting to put one in at all.
|
||||
* Since `ShoVolume`s do not attempt to check whether their `myTrader` has
|
||||
* a valid reference, they generate log spam when it does not.
|
||||
* Note that while `WeaponLocker` was supposed to do some action when
|
||||
* players enter the shop, it does not actually do anything, so we do not need
|
||||
* to care about linking lost lockers to their intended shops, we just need to
|
||||
* link some locker to shops with empty `myTrader` references. Maps with
|
||||
* custom animated (or otherwise somehow active) lockers should be themselves
|
||||
* responsible that their animations are being played correctly.
|
||||
* This fix deals with issue by either:
|
||||
* 1. Spawning our own `WeaponLocker` and assigning it to the shops
|
||||
* with empty `myTrader`;
|
||||
* 2. Finding any already existing locker on the map and linking that
|
||||
* to the shops with empty `myTrader`.
|
||||
* First way is prefereable, since we prefer not to touch placed
|
||||
* `WeaponLocker`s in case someone decides to use them correctly and makes
|
||||
* a custom class that plays welcome animation in some shop, but forgets about
|
||||
* `WeaponLocker`s for other regular shops.
|
||||
*/
|
||||
|
||||
// Remember instance of `WeaponLocker` that we spawned, so we can clean
|
||||
// it up later.
|
||||
// Reference to `WeaponLocker`
|
||||
var private NativeActorRef dummyLocker;
|
||||
// Remember fixed shops, so we can unfix them later.
|
||||
// Reference tp `ShopVolume`.
|
||||
var private array<NativeActorRef> fixedShops;
|
||||
|
||||
var LoggerAPI.Definition errNoReplacementLocker;
|
||||
|
||||
protected function Constructor()
|
||||
{
|
||||
local LevelInfo level;
|
||||
local ShopVolume nextShop;
|
||||
local WeaponLocker replacementLocker;
|
||||
level = _server.unreal.GetLevel();
|
||||
// Get locker
|
||||
replacementLocker = SpawnDummyWeaponLocker();
|
||||
if (replacementLocker == none) {
|
||||
replacementLocker = FindExistingWeaponLocker();
|
||||
}
|
||||
if (replacementLocker == none)
|
||||
{
|
||||
_.logger.Auto(errNoReplacementLocker);
|
||||
FreeSelf();
|
||||
return;
|
||||
}
|
||||
// Setup locker
|
||||
foreach level.DynamicActors(class'KFMod.ShopVolume', nextShop)
|
||||
{
|
||||
if (nextShop == none) continue;
|
||||
if (nextShop.myTrader != none) continue;
|
||||
nextShop.myTrader = replacementLocker;
|
||||
fixedShops[fixedShops.length] = _server.unreal.ActorRef(nextShop);
|
||||
}
|
||||
}
|
||||
|
||||
protected function Finalizer()
|
||||
{
|
||||
local int i;
|
||||
local ShopVolume nextShop;
|
||||
local Actor lockerInstance;
|
||||
// Free shops
|
||||
for (i = 0; i < fixedShops.length; i += 1)
|
||||
{
|
||||
nextShop = ShopVolume(fixedShops[i].Get());
|
||||
fixedShops[i].FreeSelf();
|
||||
if (nextShop != none) {
|
||||
nextShop.myTrader = none;
|
||||
}
|
||||
}
|
||||
fixedShops.length = 0;
|
||||
// Free locker
|
||||
if (dummyLocker != none) {
|
||||
lockerInstance = dummyLocker.Get();
|
||||
dummyLocker.FreeSelf();
|
||||
}
|
||||
if (lockerInstance != none) {
|
||||
lockerInstance.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private final function WeaponLocker SpawnDummyWeaponLocker()
|
||||
{
|
||||
local WeaponLocker lockerInstance;
|
||||
local bool savedCollideWorld, savedCollideActors;
|
||||
if (dummyLocker != none) {
|
||||
return WeaponLocker(dummyLocker.Get());
|
||||
}
|
||||
// Disable collision so that dummy `WeaponLocker` would both not affect
|
||||
// gameplay and would not fail spawning due to being inside terrain or
|
||||
// another actor.
|
||||
savedCollideWorld = class'WeaponLocker'.default.bCollideWorld;
|
||||
savedCollideActors = class'WeaponLocker'.default.bBlockActors;
|
||||
class'WeaponLocker'.default.bCollideWorld = false;
|
||||
class'WeaponLocker'.default.bBlockActors = false;
|
||||
lockerInstance = WeaponLocker(_.memory.Allocate(class'WeaponLocker'));
|
||||
class'WeaponLocker'.default.bCollideWorld = savedCollideWorld;
|
||||
class'WeaponLocker'.default.bBlockActors = savedCollideActors;
|
||||
if (lockerInstance != none)
|
||||
{
|
||||
lockerInstance.bHidden = true;
|
||||
lockerInstance.SetDrawType(DT_None);
|
||||
lockerInstance.SetCollision(false);
|
||||
}
|
||||
dummyLocker = _server.unreal.ActorRef(lockerInstance);
|
||||
return lockerInstance;
|
||||
}
|
||||
|
||||
private final function WeaponLocker FindExistingWeaponLocker()
|
||||
{
|
||||
local LevelInfo level;
|
||||
local WeaponLocker nextLocker;
|
||||
level = _server.unreal.GetLevel();
|
||||
foreach level.DynamicActors(class'KFMod.WeaponLocker', nextLocker)
|
||||
{
|
||||
if (nextLocker != none) {
|
||||
return nextLocker;
|
||||
}
|
||||
}
|
||||
return none;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
errNoReplacementLocker = (l=LOG_Error,m="`FixLogSpam` cannot find or spawn `WeaponLocker` actor. Shop log spam will not be disabled.")
|
||||
}
|
||||
36
kf_sources/AcediaFixes/Classes/Manifest.uc
Normal file
36
kf_sources/AcediaFixes/Classes/Manifest.uc
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Manifest for AcediaFixes package
|
||||
* Copyright 2020 - 2021 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class Manifest extends _manifest
|
||||
abstract;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
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'FixLogSpam_Feature'
|
||||
//features(9) = class'FixPipes_Feature'
|
||||
}
|
||||
107
kf_sources/AcediaFixes/Classes/PipesSafetyCollision.uc
Normal file
107
kf_sources/AcediaFixes/Classes/PipesSafetyCollision.uc
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* This collision attaches itself to pipes to catch `TakeDamage()` events
|
||||
* in their place and only propagating them after additional checks.
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class PipesSafetyCollision extends ExtendedZCollision;
|
||||
|
||||
// Static function that raplaces `PipeBombProjectile` damage detectino with
|
||||
// safer `PipesSafetyCollision`'s one.
|
||||
public final static function PipesSafetyCollision ProtectPipes(
|
||||
PipeBombProjectile target)
|
||||
{
|
||||
local PipesSafetyCollision newCollision;
|
||||
if (target == none) return none;
|
||||
|
||||
newCollision = target.Spawn(class'PipesSafetyCollision', target);
|
||||
newCollision.SetCollision(true);
|
||||
newCollision.SetCollisionSize( target.collisionRadius,
|
||||
target.collisionHeight);
|
||||
newCollision.SetLocation(target.location);
|
||||
newCollision.SetPhysics(PHYS_None);
|
||||
newCollision.SetBase(target);
|
||||
newCollision.SetTimer(1.0, true);
|
||||
target.SetCollision(false);
|
||||
return newCollision;
|
||||
}
|
||||
|
||||
// Same method for checking suspicious `Pawn`s as in `FixFFHack`.
|
||||
// Copy-pasting it once is fine-ish, but if we will need it for another fix -
|
||||
// we will have to move it out into a general method, independed from
|
||||
// particular fixes.
|
||||
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;
|
||||
}
|
||||
|
||||
// Revert changes made by caller `PipesSafetyCollision`, letting corresponding
|
||||
// pipes catch damage events on their own again.
|
||||
public final function TurnOff()
|
||||
{
|
||||
if (owner != none) {
|
||||
owner.SetCollision(true);
|
||||
}
|
||||
SetOwner(none);
|
||||
Destroy();
|
||||
}
|
||||
|
||||
// `TakeDamage()` with added checks
|
||||
function TakeDamage(
|
||||
int damage,
|
||||
Pawn instigator,
|
||||
Vector hitlocation,
|
||||
Vector momentum,
|
||||
class<DamageType> damageType,
|
||||
optional int hitIndex)
|
||||
{
|
||||
local FixPipes_Feature pipesFix;
|
||||
local PipeBombProjectile target;
|
||||
target = PipeBombProjectile(owner);
|
||||
if (target == none) return;
|
||||
pipesFix = FixPipes_Feature(
|
||||
class'FixPipes_Feature'.static.GetEnabledInstance());
|
||||
if (pipesFix == none) return;
|
||||
if (pipesFix.preventMassiveDamage && target.bTriggered) return;
|
||||
if (pipesFix.preventSuspiciousDamage && IsSuspicious(instigator)) return;
|
||||
|
||||
owner.TakeDamage( damage, instigator, hitlocation,
|
||||
momentum, damageType, hitIndex);
|
||||
}
|
||||
|
||||
event Timer()
|
||||
{
|
||||
if (owner == none) {
|
||||
Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
56
kf_sources/AcediaFixes/Classes/index.html
Normal file
56
kf_sources/AcediaFixes/Classes/index.html
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<html>
|
||||
<head><title>Index of /kf_sources/AcediaFixes/Classes/</title></head>
|
||||
<body>
|
||||
<h1>Index of /kf_sources/AcediaFixes/Classes/</h1><hr><pre><a href="../">../</a>
|
||||
<a href="AmmoPickupStalker.uc">AmmoPickupStalker.uc</a> 13-Jul-2022 13:49 3069
|
||||
<a href="FixAmmoSelling.uc">FixAmmoSelling.uc</a> 04-Jul-2022 18:49 1516
|
||||
<a href="FixAmmoSellingClass_CamoM32Pickup.uc">FixAmmoSellingClass_CamoM32Pickup.uc</a> 08-Mar-2021 12:35 1056
|
||||
<a href="FixAmmoSellingClass_CrossbowPickup.uc">FixAmmoSellingClass_CrossbowPickup.uc</a> 08-Mar-2021 12:35 1061
|
||||
<a href="FixAmmoSellingClass_GoldenM79Pickup.uc">FixAmmoSellingClass_GoldenM79Pickup.uc</a> 08-Mar-2021 12:35 1059
|
||||
<a href="FixAmmoSellingClass_LAWPickup.uc">FixAmmoSellingClass_LAWPickup.uc</a> 08-Mar-2021 12:35 1048
|
||||
<a href="FixAmmoSellingClass_M32Pickup.uc">FixAmmoSellingClass_M32Pickup.uc</a> 08-Mar-2021 12:35 1048
|
||||
<a href="FixAmmoSellingClass_M79Pickup.uc">FixAmmoSellingClass_M79Pickup.uc</a> 08-Mar-2021 12:35 1047
|
||||
<a href="FixAmmoSellingClass_PipeBombPickup.uc">FixAmmoSellingClass_PipeBombPickup.uc</a> 08-Mar-2021 12:35 1061
|
||||
<a href="FixAmmoSellingClass_SPGrenadePickup.uc">FixAmmoSellingClass_SPGrenadePickup.uc</a> 08-Mar-2021 12:35 1081
|
||||
<a href="FixAmmoSellingClass_SealSquealPickup.uc">FixAmmoSellingClass_SealSquealPickup.uc</a> 08-Mar-2021 12:35 1066
|
||||
<a href="FixAmmoSellingClass_SeekerSixPickup.uc">FixAmmoSellingClass_SeekerSixPickup.uc</a> 08-Mar-2021 12:35 1065
|
||||
<a href="FixAmmoSellingService.uc">FixAmmoSellingService.uc</a> 02-Nov-2021 21:00 2271
|
||||
<a href="FixAmmoSelling_Feature.uc">FixAmmoSelling_Feature.uc</a> 14-Jul-2022 21:17 19666
|
||||
<a href="FixDoshSpam.uc">FixDoshSpam.uc</a> 09-Sep-2022 20:53 2302
|
||||
<a href="FixDoshSpam_Feature.uc">FixDoshSpam_Feature.uc</a> 09-Sep-2022 20:59 11930
|
||||
<a href="FixDualiesCost.uc">FixDualiesCost.uc</a> 04-Jul-2022 18:50 1568
|
||||
<a href="FixDualiesCost_Feature.uc">FixDualiesCost_Feature.uc</a> 14-Jul-2022 21:18 19898
|
||||
<a href="FixFFHack.uc">FixFFHack.uc</a> 11-Jul-2022 21:44 7088
|
||||
<a href="FixFFHack_Feature.uc">FixFFHack_Feature.uc</a> 14-Jul-2022 21:18 7633
|
||||
<a href="FixInfiniteNades.uc">FixInfiniteNades.uc</a> 04-Jul-2022 18:54 1510
|
||||
<a href="FixInfiniteNades_Feature.uc">FixInfiniteNades_Feature.uc</a> 14-Jul-2022 21:18 9589
|
||||
<a href="FixInventoryAbuse.uc">FixInventoryAbuse.uc</a> 04-Jul-2022 18:55 4755
|
||||
<a href="FixInventoryAbuse_Feature.uc">FixInventoryAbuse_Feature.uc</a> 14-Jul-2022 21:24 9151
|
||||
<a href="FixLogSpam.uc">FixLogSpam.uc</a> 04-Jul-2022 18:56 1702
|
||||
<a href="FixLogSpam_Feature.uc">FixLogSpam_Feature.uc</a> 03-Nov-2021 20:49 3473
|
||||
<a href="FixPipes.uc">FixPipes.uc</a> 04-Jul-2022 18:56 2630
|
||||
<a href="FixPipes_Feature.uc">FixPipes_Feature.uc</a> 15-Jul-2022 14:39 19973
|
||||
<a href="FixProjectileFF.uc">FixProjectileFF.uc</a> 04-Jul-2022 18:56 1525
|
||||
<a href="FixProjectileFFClass_FlareRevolverProjectile.uc">FixProjectileFFClass_FlareRevolverProjectile.uc</a> 01-Jul-2022 21:57 1783
|
||||
<a href="FixProjectileFFClass_HuskGunProjectile.uc">FixProjectileFFClass_HuskGunProjectile.uc</a> 01-Jul-2022 21:57 1766
|
||||
<a href="FixProjectileFFClass_LAWProj.uc">FixProjectileFFClass_LAWProj.uc</a> 01-Jul-2022 21:57 1746
|
||||
<a href="FixProjectileFFClass_M203GrenadeProjectile.uc">FixProjectileFFClass_M203GrenadeProjectile.uc</a> 01-Jul-2022 21:57 1774
|
||||
<a href="FixProjectileFFClass_M32GrenadeProjectile.uc">FixProjectileFFClass_M32GrenadeProjectile.uc</a> 01-Jul-2022 21:57 1772
|
||||
<a href="FixProjectileFFClass_M79GrenadeProjectile.uc">FixProjectileFFClass_M79GrenadeProjectile.uc</a> 01-Jul-2022 21:57 1772
|
||||
<a href="FixProjectileFFClass_SPGrenadeProjectile.uc">FixProjectileFFClass_SPGrenadeProjectile.uc</a> 01-Jul-2022 21:57 2933
|
||||
<a href="FixProjectileFFClass_SealSquealProjectile.uc">FixProjectileFFClass_SealSquealProjectile.uc</a> 01-Jul-2022 21:57 3318
|
||||
<a href="FixProjectileFFClass_ZEDGunProjectile.uc">FixProjectileFFClass_ZEDGunProjectile.uc</a> 01-Jul-2022 21:57 1764
|
||||
<a href="FixProjectileFFClass_ZEDMKIIPrimaryProjectile.uc">FixProjectileFFClass_ZEDMKIIPrimaryProjectile.uc</a> 01-Jul-2022 21:57 1785
|
||||
<a href="FixProjectileFFClass_ZEDMKIISecondaryProjectile.uc">FixProjectileFFClass_ZEDMKIISecondaryProjectile.uc</a> 01-Jul-2022 22:00 1789
|
||||
<a href="FixProjectileFF_Feature.uc">FixProjectileFF_Feature.uc</a> 14-Jul-2022 21:23 14696
|
||||
<a href="FixSpectatorCrash.uc">FixSpectatorCrash.uc</a> 04-Jul-2022 18:57 1824
|
||||
<a href="FixSpectatorCrash_Feature.uc">FixSpectatorCrash_Feature.uc</a> 14-Jul-2022 21:20 12516
|
||||
<a href="FixZedTimeLags.uc">FixZedTimeLags.uc</a> 10-Aug-2022 11:30 1820
|
||||
<a href="FixZedTimeLags_Feature.uc">FixZedTimeLags_Feature.uc</a> 10-Aug-2022 11:44 7528
|
||||
<a href="FixedFragFire.uc">FixedFragFire.uc</a> 22-Jun-2022 11:19 1343
|
||||
<a href="HelperPickup.uc">HelperPickup.uc</a> 14-Jul-2022 21:19 9715
|
||||
<a href="HelperTrader.uc">HelperTrader.uc</a> 14-Jul-2022 21:19 6211
|
||||
<a href="Manifest.uc">Manifest.uc</a> 11-Aug-2022 21:45 1502
|
||||
<a href="PipesSafetyCollision.uc">PipesSafetyCollision.uc</a> 22-Jun-2022 11:19 3813
|
||||
</pre><hr></body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue