Browse Source

Add `FixPipes` feature for pipe bomb-related bugs

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 from and, therefore, not exploding.
master
Anton Tarasenko 4 years ago
parent
commit
d4bb598540
  1. 36
      config/AcediaFixes.ini
  2. 422
      sources/FixPipes/FixPipes.uc
  3. 39
      sources/FixPipes/MutatorListener_FixPipes.uc
  4. 105
      sources/FixPipes/PipesSafetyCollision.uc
  5. 1
      sources/Manifest.uc

36
config/AcediaFixes.ini

@ -218,6 +218,42 @@ alwaysScale=Class'KFMod.DamTypeZEDGunMKII'
; Damage types, for which we should never reaply friendly fire scaling.
;neverScale=Class'KFMod.???'
[AcediaFixes.FixPipes]
; 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.
autoEnable=true
; 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.
preventMassiveDamage=true
; It's possible for teammates to explode one's pipe under certain
; circumstances. Setting this setting to `true` will prevent that
; from happening.
preventSuspiciousDamage=true
; Setting this to `true` will prevent pipe bombs from being detonated by
; the nearby corpses on other player.
preventCorpseDetonation=true
; Setting this to `true` will prevents pipe bombs from being detonated by
; nearby KFO NPCs (Ringmaster Lockheart).
preventNPCDetonation=true
; 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`.
proximityCheckElevation=20
[AcediaFixes.FixProjectileFF]
; This feature addresses the bug that allows teammates to explode some of
; the player's projectiles by damaging them even when friendly fire is

422
sources/FixPipes/FixPipes.uc

@ -0,0 +1,422 @@
/**
* 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 extends Feature
config(AcediaFixes);
/**
* There are two main culprits for the issues we are interested in:
* `TakeDamage()` and `Timer()` methods. `TakeDamage()` lacks sufficient checks
* for instigator allows for ways to damage it, causing explosion. It also
* lacks any checks for whether pipe bom already exploded, allowing players to
* damage it several time before it's destroyed, causing explosion every time.
* `Timer()` method simply contains a porximity check for hostile pawns that
* wrongfully detects certain actor (like `KF_StoryNPC` or players' corpses)
* and cannot detect zeds around itself if pipe is placed in a way that
* prevents it from having a direct line of sight towards zeds.
* To fix our issues we have to somehow overload both of these methods,
* which is impossible while keeping Acedia server-side. So we will have to
* do some hacks.
*
* `TakeDamage()`. To override this method's behavior we will remove actor
* collision from pipe bombs, preventing it from being directly damaged, then
* add an `ExtendedZCollision` subclass around it to catch `TakeDamage()`
* events that would normally target pipe bombs themselves. There we can add
* additional checks to help us decide whether pipe bombs should actually
* be damaged.
*
* `Timer()`. It would be simple to take control of this method by, say,
* spamming `SetTimer(0.0, false)` every tick and then executing our own logic
* elsewhere. However we only care about changin `Timer()`'s logic when pipe
* bomb attempts to detect cnearby enemies and do not want to manage the rest
* of `Timer()`'s logic.
* Pipe bombs work like this: after being thrown they fall until they land,
* invoking `Landed()` and `HitWall()` calls that start up the timer. Then
* timer uses `ArmingCountDown` variable to count down the time for pipe to
* "arm", that is, start functioning. It's after that that pipe starts to try
* and detect nearby enemies until it decides to start exploding sequence,
* instantly exploded with damage or disintegrated by siren's scream.
* We want to catch the moment when `ArmingCountDown` reaches zero and
* only then disable the timer. Then case we'll only need to reimplement logic
* that takes care of enemy detection. We also do not need to constantly
* re-reset the timer, since any code that will restart it would mean that
* pipe bomb no longer needs to look for enemies.
* When we detect enough enemies nearby or a change in pipe bomb's state
* due to outside factors (being exploded or disintegrated) we simply need to
* start it's the timer (if it was not done for us already).
*/
// NOTE #1: setting either of `preventMassiveDamage` or
// `preventSuspiciousDamage` might change how and where pipes might fall on
// the ground (for example, pipe hats shoul not work anymore). In most cases,
// however, there should be no difference from vanilla gameplay.
// Setting this to `true` fixes a mechanic that allows pipes to deal extra
// damage when detonated with a high fire rate weapons: pipes will now always
// deal the same amount of damage.
var public const config bool preventMassiveDamage;
// It's possible for teammates to explode one's pipe under certain
// circumstances. Setting this setting to `true` will prevent that
// from happening.
var public const config bool preventSuspiciousDamage;
// Setting this to `true` will prevent pipe bombs from being detonated by
// the nearby corpses on other player.
var public const config bool preventCorpseDetonation;
// Setting this to `true` will prevents pipe bombs from being detonated by
// nearby KFO NPCs (Ringmaster Lockheart).
var public const config bool preventNPCDetonation;
// Certain spots prevent pipe bombs from having a line of sight to the
// nearby zeds, preventing them from ever exploding. This issue can be resolves
// by checking zed's proximity not to the pipe itself, but to a point directly
// above it (20 units above by default).
// If you wish to disable this part of the feature - set it to zero or
// negative value. Otherwise it is suggested to keep it at `20`.
var public const config float proximityCheckElevation;
// Since this feature needs to do proximity check instead of pipes, we will
// need to track all the pipes we will be doing checks for.
// This struct contains all reference to the pipe itself and everything
// else we need to track.
struct PipeRecord
{
// Pipe that this record tracks
var PipeBombProjectile 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.
var PipesSafetyCollision 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;
protected function OnEnabled()
{
local PipeBombProjectile nextPipe;
pipesRelevancyFlag = class'PipeBombProjectile'.default.bAlwaysRelevant;
class'PipeBombProjectile'.default.bGameRelevant = false;
// Fix pipes that are already lying about on the map
foreach level.DynamicActors(class'KFMod.PipeBombProjectile', nextPipe) {
RegisterPipe(nextPipe);
}
}
protected function OnDisabled()
{
local int i;
class'PipeBombProjectile'.default.bGameRelevant = pipesRelevancyFlag;
for (i = 0; i < pipeRecords.length; i += 1) {
ReleasePipe(pipeRecords[i]);
}
pipeRecords.length = 0;
}
// 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 == newPipe) {
return;
}
}
newRecord.pipe = newPipe;
// Setup `PipesSafetyCollision` for catching `TakeDamage()` events
// (only if we need to according to settings)
if (NeedSafetyCollision())
{
newRecord.safetyCollision =
class'PipesSafetyCollision'.static.ProtectPipes(newPipe);
}
newRecord.pipe = 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)
{
if (pipeRecord.safetyCollision != none)
{
pipeRecord.safetyCollision.TurnOff();
pipeRecord.safetyCollision = none;
}
if (pipeRecord.proximityCheckIntercepted && pipeRecord.pipe != none)
{
pipeRecord.proximityCheckIntercepted = false;
if (IsPipeDoingProximityChecks(pipeRecord.pipe)) {
pipeRecord.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()
{
local int i;
while (i < pipeRecords.length)
{
if (pipeRecords[i].pipe == none) {
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;
for (i = 0; i < pipeRecords.length; i += 1)
{
if (pipeRecords[i].proximityCheckIntercepted) continue;
if (pipeRecords[i].pipe == none) continue;
if (IsPipeDoingProximityChecks(pipeRecords[i].pipe))
{
pipeRecords[i].pipe.SetTimer(0, false);
// 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;
for (i = 0; i < pipeRecords.length; i += 1)
{
if (pipeRecords[i].pipe == none) continue;
// `timerCountDown` does not makes sense for pipes that
// are not doing proxiity checks
if (!IsPipeDoingProximityChecks(pipeRecords[i].pipe)) continue;
pipeRecords[i].timerCountDown -= delta;
if (pipeRecords[i].timerCountDown <= 0)
{
checkLocation = pipeRecords[i].pipe.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);
}
}
}
// Assumes `pipeRecord.pipe != none`
// 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 = pipeRecord.pipe;
pipe.bAlwaysRelevant = false;
pipe.PlaySound(pipe.beepSound,, 0.5,, 50.0);
// Out rewritten logic, which should do exactly the same:
foreach 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)
{
CleanPipeRecords();
if (NeedManagedProximityChecks())
{
InterceptProximityChecks();
PerformProximityChecks(delta);
}
}
defaultproperties
{
preventMassiveDamage = true
preventSuspiciousDamage = true
onlyZedsAndPlayersCanDetonate = true
preventCorpseDetonation = true
preventNPCDetonation = true
proximityCheckElevation = 20.0
// Listeners
requiredListeners(0) = class'MutatorListener_FixPipes'
}

39
sources/FixPipes/MutatorListener_FixPipes.uc

@ -0,0 +1,39 @@
/**
* Overloaded mutator events listener to register spawned pipe bombs.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MutatorListener_FixPipes extends MutatorListenerBase
abstract;
static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
{
local FixPipes pipesFix;
local PipeBombProjectile pipeProjectile;
pipeProjectile = PipeBombProjectile(other);
if (pipeProjectile == none) return true;
pipesFix = FixPipes(class'FixPipes'.static.GetInstance());
if (pipesFix == none) return true;
pipesFix.RegisterPipe(pipeProjectile);
return true;
}
defaultproperties
{
relatedEvents = class'MutatorEvents'
}

105
sources/FixPipes/PipesSafetyCollision.uc

@ -0,0 +1,105 @@
/**
* 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 pipesFix;
local PipeBombProjectile target;
target = PipeBombProjectile(owner);
if (target == none) return;
pipesFix = FixPipes(class'FixPipes'.static.GetInstance());
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
{
}

1
sources/Manifest.uc

@ -31,4 +31,5 @@ defaultproperties
features(6) = class'FixDualiesCost'
features(7) = class'FixInventoryAbuse'
features(8) = class'FixProjectileFF'
features(9) = class'FixPipes'
}
Loading…
Cancel
Save