diff --git a/config/AcediaFixes.ini b/config/AcediaFixes.ini
index 639ded2..7acc9b9 100644
--- a/config/AcediaFixes.ini
+++ b/config/AcediaFixes.ini
@@ -218,6 +218,16 @@ alwaysScale=Class'KFMod.DamTypeZEDGunMKII'
; Damage types, for which we should never reaply friendly fire scaling.
;neverScale=Class'KFMod.???'
+[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
+; turned off, therefore killing the player (whether by accident or not).
+autoEnable=true
+; 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.
+ignoreFriendlyFire=false
[AcediaFixes.FixZedTimeLags]
; When zed time activates, game speed is immediately set to
diff --git a/sources/FixProjectileFF/FixProjectileFF.uc b/sources/FixProjectileFF/FixProjectileFF.uc
new file mode 100644
index 0000000..6e964ea
--- /dev/null
+++ b/sources/FixProjectileFF/FixProjectileFF.uc
@@ -0,0 +1,165 @@
+/**
+ * This feature addresses the bug that allows teammates to explode some of
+ * the player's projectiles by damaging them even when friendly fire is
+ * turned off, therefore killing the player (whether by accident or not).
+ *
+ * Problem is solved by "disarming" projectiles vulnerable to this
+ * friendly fire and replacing them with our own class of projectile that is
+ * spawned only on a server and does additional safety checks to ensure it will
+ * only explode when it is expected from it.
+ * Copyright 2021 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class FixProjectileFF extends Feature
+ config(AcediaFixes);
+
+/**
+ * All projectiles vulnerable to this bug (we consider only those that can
+ * explode and harm the player) are derived from `ROBallisticProjectile`. When
+ * one of them is spawned we will:
+ * 1. Set it's damage parameters to zero, to ensure that it won't damage
+ * anyone or anything if other measures fail;
+ * 2. Disable it's collision, preventing it from being accessed via
+ * `VisibleCollidingActors()` method that is usually used to damage
+ * these projectiles.
+ * Then we spawn our own version of the projectile with fixed
+ * `TakeDamage()` that does additional check that either it's projectile's
+ * owner that damages it or friendly fire is enabled.
+ * To do this replacement we will need to catch the moment when vanilla
+ * projectile spawns. Unfortunately, this cannot be done by default, since all
+ * projectiles have `bGameRelevant` flag set to `true`, which prevents
+ * `CheckReplacement()` from being called for them. So, when feature is
+ * enabled, it forces all the projectiles we are interested in to have
+ * `bGameRelevant = false`.
+ *
+ * Issue arises from the fact that these two projectiles can desyncronize:
+ * old projectile, that client sees, might not be in the same location as the
+ * new one (especially since we are disabling collisions for the old one), that
+ * deals damage. There are two cases to consider, depending on
+ * the `bNetTemporary` of the old projectile:
+ * 1. `bNetTemporary == true`: projectile version on client side is
+ * independent from the server-side's one. In this case there is
+ * nothing we can do. In fact, vanilla game suffers from this very bug
+ * that makes, say, M79 projectile fly past the zed it exploded
+ * (usually after killing it). To deal with this we would need
+ * the ability to affect client's code, which cannot be done in
+ * the server mode.
+ * 2. `bNetTemporary == true`: projectile version on client side is
+ * actually syncronized 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 repalced with what (protected class). It also remembers the
+// previous state of `bGameRelevant` for replacable class to restore it in case
+// this feature is disabled.
+struct ReplacementRule
+{
+ // `bGameRelevant` before this feature set it to `false`
+ var bool relevancyFlag;
+ var class vulnerableClass;
+ var class protectedClass;
+};
+var private const array rules;
+
+protected function OnEnabled()
+{
+ local int i;
+ for (i = 0; i < rules.length; i += 1)
+ {
+ if (rules[i].vulnerableClass == none) continue;
+ if (rules[i].protectedClass == none) continue;
+ rules[i].relevancyFlag = rules[i].vulnerableClass.default.bGameRelevant;
+ rules[i].vulnerableClass.default.bGameRelevant = false;
+ }
+}
+
+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;
+ }
+}
+
+// Returns "fixed" class that no longer explodes from random damage
+public final static function class FindFixedClass(
+ class projectileClass)
+{
+ local int i;
+ local array rulesCopy;
+ if (projectileClass == none) return none;
+
+ rulesCopy = default.rules;
+ for (i = 0; i < rulesCopy.length; i += 1)
+ {
+ if (rulesCopy[i].vulnerableClass == projectileClass) {
+ return rulesCopy[i].protectedClass;
+ }
+ }
+ return projectileClass;
+}
+
+// Check if, according to this fix, projectiles should explode from
+// friendly fire
+// If `FixProjectileFF` in disabled always returns `false`.
+public final static function bool IsFriendlyFireAcceptable()
+{
+ local TeamGame gameType;
+ local FixProjectileFF projectileFFFix;
+ projectileFFFix = FixProjectileFF(GetInstance());
+ if (projectileFFFix == none) return false;
+ if (projectileFFFix.ignoreFriendlyFire) return false;
+ if (projectileFFFix.level == none) return false;
+ gameType = TeamGame(projectileFFFix.level.game);
+ if (gameType == none) return false;
+
+ return gameType.friendlyFireScale > 0;
+}
+
+defaultproperties
+{
+ ignoreFriendlyFire = false
+ rules(0) = (vulnerableClass=class'KFMod.M79GrenadeProjectile',protectedClass=class'FixProjectileFFClass_M79GrenadeProjectile')
+ rules(1) = (vulnerableClass=class'KFMod.M32GrenadeProjectile',protectedClass=class'FixProjectileFFClass_M32GrenadeProjectile')
+ rules(2) = (vulnerableClass=class'KFMod.LAWProj',protectedClass=class'FixProjectileFFClass_LAWProj')
+ rules(3) = (vulnerableClass=class'KFMod.M203GrenadeProjectile',protectedClass=class'FixProjectileFFClass_M203GrenadeProjectile')
+ rules(4) = (vulnerableClass=class'KFMod.ZEDGunProjectile',protectedClass=class'FixProjectileFFClass_ZEDGunProjectile')
+ rules(5) = (vulnerableClass=class'KFMod.ZEDMKIIPrimaryProjectile',protectedClass=class'FixProjectileFFClass_ZEDMKIIPrimaryProjectile')
+ rules(6) = (vulnerableClass=class'KFMod.ZEDMKIISecondaryProjectile',protectedClass=class'FixProjectileFFClass_ZEDMKIISecondaryProjectile')
+ rules(7) = (vulnerableClass=class'KFMod.FlareRevolverProjectile',protectedClass=class'FixProjectileFFClass_FlareRevolverProjectile')
+ rules(8) = (vulnerableClass=class'KFMod.HuskGunProjectile',protectedClass=class'FixProjectileFFClass_HuskGunProjectile')
+ rules(9) = (vulnerableClass=class'KFMod.SPGrenadeProjectile',protectedClass=class'FixProjectileFFClass_SPGrenadeProjectile')
+ rules(10) = (vulnerableClass=class'KFMod.SealSquealProjectile',protectedClass=class'FixProjectileFFClass_SealSquealProjectile')
+ // Listeners
+ requiredListeners(0) = class'MutatorListener_FixProjectileFF'
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_FlareRevolverProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_FlareRevolverProjectile.uc
new file mode 100644
index 0000000..46b8831
--- /dev/null
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_FlareRevolverProjectile.uc
@@ -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 .
+ */
+class FixProjectileFFClass_FlareRevolverProjectile
+ extends FlareRevolverProjectile;
+
+function TakeDamage(
+ int damage,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ Vector momentum,
+ class damageType,
+ optional int hitIndex)
+{
+ local bool canTakeThisDamage;
+ if (damageType == class'SirenScreamDamage')
+ {
+ Disintegrate(hitLocation, Vect(0, 0, 1));
+ return;
+ }
+ canTakeThisDamage =
+ (instigatedBy == instigator)
+ || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ if (canTakeThisDamage && !bDud) {
+ Explode(hitLocation, Vect(0, 0, 0));
+ }
+}
+
+defaultproperties
+{
+ RemoteRole = ROLE_None
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_HuskGunProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_HuskGunProjectile.uc
new file mode 100644
index 0000000..f79d77d
--- /dev/null
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_HuskGunProjectile.uc
@@ -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 .
+ */
+class FixProjectileFFClass_HuskGunProjectile extends HuskGunProjectile;
+
+function TakeDamage(
+ int damage,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ Vector momentum,
+ class damageType,
+ optional int hitIndex)
+{
+ local bool canTakeThisDamage;
+ if (damageType == class'SirenScreamDamage')
+ {
+ Disintegrate(hitLocation, Vect(0, 0, 1));
+ return;
+ }
+ canTakeThisDamage =
+ (instigatedBy == instigator)
+ || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ if (canTakeThisDamage && !bDud) {
+ Explode(hitLocation, Vect(0, 0, 0));
+ }
+}
+
+defaultproperties
+{
+ RemoteRole = ROLE_None
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_LAWProj.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_LAWProj.uc
new file mode 100644
index 0000000..d823df2
--- /dev/null
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_LAWProj.uc
@@ -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 .
+ */
+class FixProjectileFFClass_LAWProj extends LAWProj;
+
+function TakeDamage(
+ int damage,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ Vector momentum,
+ class damageType,
+ optional int hitIndex)
+{
+ local bool canTakeThisDamage;
+ if (damageType == class'SirenScreamDamage')
+ {
+ Disintegrate(hitLocation, Vect(0, 0, 1));
+ return;
+ }
+ canTakeThisDamage =
+ (instigatedBy == instigator)
+ || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ if (canTakeThisDamage && !bDud) {
+ Explode(hitLocation, Vect(0, 0, 0));
+ }
+}
+
+defaultproperties
+{
+ RemoteRole = ROLE_None
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M203GrenadeProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M203GrenadeProjectile.uc
new file mode 100644
index 0000000..5664c19
--- /dev/null
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M203GrenadeProjectile.uc
@@ -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 .
+ */
+class FixProjectileFFClass_M203GrenadeProjectile extends M203GrenadeProjectile;
+
+function TakeDamage(
+ int damage,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ Vector momentum,
+ class damageType,
+ optional int hitIndex)
+{
+ local bool canTakeThisDamage;
+ if (damageType == class'SirenScreamDamage')
+ {
+ Disintegrate(hitLocation, Vect(0, 0, 1));
+ return;
+ }
+ canTakeThisDamage =
+ (instigatedBy == instigator)
+ || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ if (canTakeThisDamage && !bDud) {
+ Explode(hitLocation, Vect(0, 0, 0));
+ }
+}
+
+defaultproperties
+{
+ RemoteRole = ROLE_None
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M32GrenadeProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M32GrenadeProjectile.uc
new file mode 100644
index 0000000..8c5179c
--- /dev/null
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M32GrenadeProjectile.uc
@@ -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 .
+ */
+class FixProjectileFFClass_M32GrenadeProjectile extends M32GrenadeProjectile;
+
+function TakeDamage(
+ int damage,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ Vector momentum,
+ class damageType,
+ optional int hitIndex)
+{
+ local bool canTakeThisDamage;
+ if (damageType == class'SirenScreamDamage')
+ {
+ Disintegrate(hitLocation, Vect(0, 0, 1));
+ return;
+ }
+ canTakeThisDamage =
+ (instigatedBy == instigator)
+ || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ if (canTakeThisDamage && !bDud) {
+ Explode(hitLocation, Vect(0, 0, 0));
+ }
+}
+
+defaultproperties
+{
+ RemoteRole = ROLE_None
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M79GrenadeProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M79GrenadeProjectile.uc
new file mode 100644
index 0000000..9eaa34b
--- /dev/null
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_M79GrenadeProjectile.uc
@@ -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 .
+ */
+class FixProjectileFFClass_M79GrenadeProjectile extends M79GrenadeProjectile;
+
+function TakeDamage(
+ int damage,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ Vector momentum,
+ class damageType,
+ optional int hitIndex)
+{
+ local bool canTakeThisDamage;
+ if (damageType == class'SirenScreamDamage')
+ {
+ Disintegrate(hitLocation, Vect(0, 0, 1));
+ return;
+ }
+ canTakeThisDamage =
+ (instigatedBy == instigator)
+ || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ if (canTakeThisDamage && !bDud) {
+ Explode(hitLocation, Vect(0, 0, 0));
+ }
+}
+
+defaultproperties
+{
+ RemoteRole = ROLE_None
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SPGrenadeProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SPGrenadeProjectile.uc
new file mode 100644
index 0000000..f4b575c
--- /dev/null
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SPGrenadeProjectile.uc
@@ -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 .
+ */
+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,
+ optional int hitIndex)
+{
+ local bool canTakeThisDamage;
+ if (damageType == class'SirenScreamDamage')
+ {
+ Disintegrate(hitLocation, Vect(0, 0, 1));
+ return;
+ }
+ canTakeThisDamage =
+ (instigatedBy == instigator)
+ || class'FixProjectileFF'.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
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SealSquealProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SealSquealProjectile.uc
new file mode 100644
index 0000000..0799922
--- /dev/null
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_SealSquealProjectile.uc
@@ -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 .
+ */
+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,
+ optional int hitIndex)
+{
+ local bool canTakeThisDamage;
+ if (damageType == class'SirenScreamDamage')
+ {
+ Disintegrate(hitLocation, Vect(0, 0, 1));
+ return;
+ }
+ canTakeThisDamage =
+ (instigatedBy == instigator)
+ || class'FixProjectileFF'.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
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDGunProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDGunProjectile.uc
new file mode 100644
index 0000000..2beb6bc
--- /dev/null
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDGunProjectile.uc
@@ -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 .
+ */
+class FixProjectileFFClass_ZEDGunProjectile extends ZEDGunProjectile;
+
+function TakeDamage(
+ int damage,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ Vector momentum,
+ class damageType,
+ optional int hitIndex)
+{
+ local bool canTakeThisDamage;
+ if (damageType == class'SirenScreamDamage')
+ {
+ Disintegrate(hitLocation, Vect(0, 0, 1));
+ return;
+ }
+ canTakeThisDamage =
+ (instigatedBy == instigator)
+ || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ if (canTakeThisDamage && !bDud) {
+ Explode(hitLocation, Vect(0, 0, 0));
+ }
+}
+
+defaultproperties
+{
+ RemoteRole = ROLE_None
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIIPrimaryProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIIPrimaryProjectile.uc
new file mode 100644
index 0000000..a9ec190
--- /dev/null
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIIPrimaryProjectile.uc
@@ -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 .
+ */
+class FixProjectileFFClass_ZEDMKIIPrimaryProjectile
+ extends ZEDMKIIPrimaryProjectile;
+
+function TakeDamage(
+ int damage,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ Vector momentum,
+ class damageType,
+ optional int hitIndex)
+{
+ local bool canTakeThisDamage;
+ if (damageType == class'SirenScreamDamage')
+ {
+ Disintegrate(hitLocation, Vect(0, 0, 1));
+ return;
+ }
+ canTakeThisDamage =
+ (instigatedBy == instigator)
+ || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ if (canTakeThisDamage && !bDud) {
+ Explode(hitLocation, Vect(0, 0, 0));
+ }
+}
+
+defaultproperties
+{
+ RemoteRole = ROLE_None
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIISecondaryProjectile.uc b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIISecondaryProjectile.uc
new file mode 100644
index 0000000..673806e
--- /dev/null
+++ b/sources/FixProjectileFF/FixedClasses/FixProjectileFFClass_ZEDMKIISecondaryProjectile.uc
@@ -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 .
+ */
+class FixProjectileFFClass_ZEDMKIISecondaryProjectile extends ZEDGunProjectile;
+
+function TakeDamage(
+ int damage,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ Vector momentum,
+ class damageType,
+ optional int hitIndex)
+{
+ local bool canTakeThisDamage;
+ if (damageType == class'SirenScreamDamage')
+ {
+ Disintegrate(hitLocation, Vect(0, 0, 1));
+ return;
+ }
+ canTakeThisDamage =
+ (instigatedBy == instigator)
+ || class'FixProjectileFF'.static.IsFriendlyFireAcceptable();
+ if (canTakeThisDamage && !bDud) {
+ Explode(hitLocation, Vect(0, 0, 0));
+ }
+}
+
+defaultproperties
+{
+ RemoteRole = ROLE_None
+}
\ No newline at end of file
diff --git a/sources/FixProjectileFF/MutatorListener_FixProjectileFF.uc b/sources/FixProjectileFF/MutatorListener_FixProjectileFF.uc
new file mode 100644
index 0000000..a36eca3
--- /dev/null
+++ b/sources/FixProjectileFF/MutatorListener_FixProjectileFF.uc
@@ -0,0 +1,167 @@
+/**
+ * Overloaded mutator events listener to replace projectiles vulnerable to
+ * friendly fire.
+ * Copyright 2021 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class MutatorListener_FixProjectileFF extends MutatorListenerBase
+ abstract;
+
+static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
+{
+ local ROBallisticProjectile projectile;
+ local class newClass;
+ projectile = ROBallisticProjectile(other);
+ if (projectile == none) return true;
+
+ projectile.bGameRelevant = true;
+ newClass =
+ class'FixProjectileFF'.static.FindFixedClass(projectile.class);
+ if (newClass == none || newClass == projectile.class) {
+ return true;
+ }
+ ReplaceProjectileWith(ROBallisticProjectile(other), newClass);
+ return true;
+}
+
+static function ReplaceProjectileWith(
+ ROBallisticProjectile oldProjectile,
+ class replacementClass)
+{
+ local Pawn instigator;
+ local ROBallisticProjectile newProjectile;
+ if (oldProjectile == none) return;
+ if (replacementClass == none) return;
+ instigator = oldProjectile.instigator;
+ if (instigator == none) return;
+ newProjectile = instigator.Spawn( replacementClass,,,
+ oldProjectile.location,
+ oldProjectile.rotation);
+ if (newProjectile == none) return;
+
+ // Move projectile damage data to make sure new projectile deals
+ // exactly the same damage as the old one and the old one no longer
+ // deals any.
+ // Technically we only need to zero `oldProjectile` damage values for
+ // most weapons, since they are automatically the same for the
+ // new projectile.
+ // However `KFMod.HuskGun` is an exception that changes these values
+ // depending of the charge, so we need to either consider this as a
+ // special case or just move valkues 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 mimick
+ // former's movements as close as possible
+ SetupOldProjectileAsFace(oldProjectile, newProjectile);
+}
+
+// Make old projectile behave as close to the new one as possible
+static function SetupOldProjectileAsFace(
+ ROBallisticProjectile oldProjectile,
+ ROBallisticProjectile newProjectile)
+{
+ local FixProjectileFFClass_SPGrenadeProjectile newProjectileAsOrca;
+ local FixProjectileFFClass_SealSquealProjectile newProjectileAsHarpoon;
+ if (oldProjectile == none) return;
+ if (newProjectile == none) return;
+
+ // Removing collisions:
+ // 1. Avoids unnecessary bumping into zeds and other `Actor`s;
+ // 2. Removes `oldProjectile` from being accessible by
+ // `VisibleCollidingActors()`, therefore avoiding unwanted
+ // `TakeDamage()` calls from friendly fire.
+ oldProjectile.SetCollision(false, false);
+ // Prevent `oldProjectile` from dealing explosion damage in case something
+ // still damages it
+ oldProjectile.bHurtEntry = true;
+ // We can only make client-side projectile follow 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 sepratly defined in 4 different base classes of interest
+// and moving (or zeroing) it is cumbersome enough to warrant a separate method
+static function MoveImpactDamage(
+ ROBallisticProjectile oldProjectile,
+ ROBallisticProjectile newProjectile)
+{
+ local LAWProj oldProjectileAsLaw, newProjectileAsLaw;
+ local M79GrenadeProjectile oldProjectileAsM79, newProjectileAsM79;
+ local SPGrenadeProjectile oldProjectileAsOrca, newProjectileAsOrca;
+ local SealSquealProjectile oldProjectileAsHarpoon, newProjectileAsHarpoon;
+ if (oldProjectile == none) return;
+ if (newProjectile == none) return;
+
+ // L.A.W + derivatives:
+ // Zed guns, Flare Revolver, Husk Gun
+ oldProjectileAsLaw = LawProj(oldProjectile);
+ newProjectileAsLaw = LawProj(newProjectile);
+ if (oldProjectileAsLaw != none && newProjectileAsLaw != none)
+ {
+ newProjectileAsLaw.impactDamage = oldProjectileAsLaw.impactDamage;
+ oldProjectileAsLaw.impactDamage = 0;
+ return;
+ }
+ // M79 Grenade Launcher + derivatives:
+ // M32, M4 203
+ oldProjectileAsM79 = M79GrenadeProjectile(oldProjectile);
+ newProjectileAsM79 = M79GrenadeProjectile(newProjectile);
+ if (oldProjectileAsM79 != none && newProjectileAsM79 != none)
+ {
+ newProjectileAsM79.impactDamage = oldProjectileAsM79.impactDamage;
+ oldProjectileAsM79.impactDamage = 0;
+ return;
+ }
+ // The Orca Bomb Propeller
+ oldProjectileAsOrca = SPGrenadeProjectile(oldProjectile);
+ newProjectileAsOrca = SPGrenadeProjectile(newProjectile);
+ if (oldProjectileAsOrca != none && newProjectileAsOrca != none)
+ {
+ newProjectileAsOrca.impactDamage = oldProjectileAsOrca.impactDamage;
+ oldProjectileAsOrca.impactDamage = 0;
+ return;
+ }
+ // SealSqueal Harpoon Bomber
+ oldProjectileAsHarpoon = SealSquealProjectile(oldProjectile);
+ newProjectileAsHarpoon = SealSquealProjectile(newProjectile);
+ if (oldProjectileAsHarpoon != none && newProjectileAsHarpoon != none)
+ {
+ newProjectileAsHarpoon.impactDamage = oldProjectileAsHarpoon.impactDamage;
+ oldProjectileAsHarpoon.impactDamage = 0;
+ return;
+ }
+}
+
+defaultproperties
+{
+ relatedEvents = class'MutatorEvents'
+}
\ No newline at end of file
diff --git a/sources/Manifest.uc b/sources/Manifest.uc
index 4276afb..b17c834 100644
--- a/sources/Manifest.uc
+++ b/sources/Manifest.uc
@@ -1,6 +1,6 @@
/**
* Manifest for AcediaFixes package
- * Copyright 2020 Anton Tarasenko
+ * Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -30,4 +30,5 @@ defaultproperties
features(5) = class'FixSpectatorCrash'
features(6) = class'FixDualiesCost'
features(7) = class'FixInventoryAbuse'
+ features(8) = class'FixProjectileFF'
}
\ No newline at end of file