diff --git a/sources/FixLogSpam/FixLogSpam.uc b/sources/FixLogSpam/FixLogSpam.uc
new file mode 100644
index 0000000..93fbb38
--- /dev/null
+++ b/sources/FixLogSpam/FixLogSpam.uc
@@ -0,0 +1,62 @@
+/**
+ * This feature fixes different instances of log spam by the killing floor
+ * with various warnings and errors. Some of them have actual underlying bugs
+ * that need to be fixed, but a lot seem to be just a byproduct of dead and
+ * abandoned features or simple negligence.
+ * Whatever the case, now that TWI will no longer make any new changes to
+ * the game a lot of them do not serve any purpose and simply pollute
+ * log files. We try to get rid of at least some of them.
+ * Since changes we make do not actually have gameplay effect and
+ * are more aimed at convenience of server owners, our philosophy with the
+ * changes will be to avoid solutions that are way too "hacky" and prefer some
+ * message spam getting through to the possibility of some unexpected gameplay
+ * effects as far as vanilla game is concerned.
+ * Copyright 2021 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class FixLogSpam extends Feature
+ config(AcediaFixes);
+
+// This is responsible for fixing log spam due to picking up dropped
+// weapons without set `inventory` variable.
+var private const bool fixPickupSpam;
+var private HelperPickup helperPickupSpam;
+
+protected function OnEnabled()
+{
+ if (fixPickupSpam) {
+ helperPickupSpam = HelperPickup(_.memory.Allocate(class'HelperPickup'));
+ }
+}
+
+protected function OnDisabled()
+{
+ _.memory.Free(helperPickupSpam);
+ helperPickupSpam = none;
+}
+
+public function Tick(float delta)
+{
+ if (helperPickupSpam != none) {
+ helperPickupSpam.Tick();
+ }
+}
+
+defaultproperties
+{
+ fixPickupSpam = true
+}
\ No newline at end of file
diff --git a/sources/FixLogSpam/SpamPickup/HelperPickup.uc b/sources/FixLogSpam/SpamPickup/HelperPickup.uc
new file mode 100644
index 0000000..363f7cc
--- /dev/null
+++ b/sources/FixLogSpam/SpamPickup/HelperPickup.uc
@@ -0,0 +1,213 @@
+/**
+ * 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 .
+ */
+class HelperPickup extends AcediaObject
+ config(AcediaFixes);
+
+/**
+ * `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(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 recordedPickups;
+// Pickups that will get `bDropped` flag checked the next tick to
+// determine if we need to fix them.
+var private array 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)
+ _.unreal.AddGameRules(class'PickupSpamRule');
+ // To detect newly spawned pickups
+ class'MutatorListener_FixLogSpam_Pickup'.static.SetActive(true);
+ // 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 = _.unreal.GetLevel();
+ foreach level.DynamicActors(class'KFMod.KFWeaponPickup', nextPickup) {
+ pendingPickups[pendingPickups.length] = nextPickup;
+ }
+}
+
+protected function Finalizer()
+{
+ local int i;
+ for (i = 0; i < recordedPickups.length; i += 1)
+ {
+ if (recordedPickups[i] != none && !recordedPickups[i].bPendingDelete) {
+ recordedPickups[i].Enable('Destroyed');
+ }
+ }
+ recordedPickups.length = 0;
+ pendingPickups.length = 0;
+ _.unreal.RemoveGameRules(class'PickupSpamRule');
+ class'MutatorListener_FixLogSpam_Pickup'.static.SetActive(false);
+}
+
+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;
+ if (pickupToCheck == none) {
+ return false;
+ }
+ for (i = 0; i < recordedPickups.length; i += 1)
+ {
+ if (recordedPickups[i] == pickupToCheck) {
+ return true;
+ }
+ }
+ for (i = 0; i < pendingPickups.length; i += 1)
+ {
+ if (pendingPickups[i] == 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] == none) {
+ 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] = newPickup;
+ }
+ else {
+ pendingPickups[pendingPickups.length] = newPickup;
+ }
+}
+
+// Re-disables `Destroyed()` event for all recorded (confirmed) dropped pickups
+// and processes all pending (for the check if they are dropped) pickups.
+public final function UpdatePickups()
+{
+ local int i;
+ for (i = 0; i < recordedPickups.length; i += 1)
+ {
+ if (recordedPickups[i] != none) {
+ recordedPickups[i].Disable('Destroyed');
+ }
+ }
+ for (i = 0; i < pendingPickups.length; i += 1)
+ {
+ if (pendingPickups[i].bDropped)
+ {
+ pendingPickups[i].Disable('Destroyed');
+ recordedPickups[recordedPickups.length] = pendingPickups[i];
+ }
+ }
+ pendingPickups.length = 0;
+}
+
+public final function Tick()
+{
+ CleanRecordedPickups();
+ UpdatePickups();
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/FixLogSpam/SpamPickup/MutatorListener_FixLogSpam_Pickup.uc b/sources/FixLogSpam/SpamPickup/MutatorListener_FixLogSpam_Pickup.uc
new file mode 100644
index 0000000..f47ef5b
--- /dev/null
+++ b/sources/FixLogSpam/SpamPickup/MutatorListener_FixLogSpam_Pickup.uc
@@ -0,0 +1,40 @@
+/**
+ * Overloaded mutator events listener to catch and, possibly,
+ * prevent spawning `KFWeaponPickup` for fixing log spam related to them.
+ * Copyright 2021 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class MutatorListener_FixLogSpam_Pickup extends MutatorListenerBase
+ abstract;
+
+static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
+{
+ local HelperPickup helper;
+ local KFWeaponPickup otherPickup;
+ otherPickup = KFWeaponPickup(other);
+ if (otherPickup == none) return true;
+ helper = class'HelperPickup'.static.GetInstance();
+ if (helper == none) return true;
+
+ helper.HandlePickup(otherPickup);
+ return true;
+}
+
+defaultproperties
+{
+ relatedEvents = class'MutatorEvents'
+}
\ No newline at end of file
diff --git a/sources/FixLogSpam/SpamPickup/PickupSpamRule.uc b/sources/FixLogSpam/SpamPickup/PickupSpamRule.uc
new file mode 100644
index 0000000..f2fd8b7
--- /dev/null
+++ b/sources/FixLogSpam/SpamPickup/PickupSpamRule.uc
@@ -0,0 +1,38 @@
+/**
+ * This rule detects when someone is trying to... pick up a pickup and
+ * forces additional fixing update on all the ones, dropped by players.
+ * 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 PickupSpamRule extends GameRules;
+
+function bool OverridePickupQuery(
+ Pawn toucher,
+ Pickup touchedPickup,
+ out byte allowPickup)
+{
+ local HelperPickup helper;
+ helper = class'HelperPickup'.static.GetInstance();
+ if (helper != none) {
+ helper.UpdatePickups();
+ }
+ return super.OverridePickupQuery(toucher, touchedPickup, allowPickup);
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Manifest.uc b/sources/Manifest.uc
index 538e6ab..8f87fd2 100644
--- a/sources/Manifest.uc
+++ b/sources/Manifest.uc
@@ -22,14 +22,15 @@
defaultproperties
{
- features(0) = class'FixZedTimeLags'
- features(1) = class'FixDoshSpam'
- features(2) = class'FixFFHack'
- features(3) = class'FixInfiniteNades'
- features(4) = class'FixAmmoSelling'
- features(5) = class'FixSpectatorCrash'
- features(6) = class'FixDualiesCost'
- features(7) = class'FixInventoryAbuse'
- features(8) = class'FixProjectileFF'
- features(9) = class'FixPipes'
+ features(0) = class'FixZedTimeLags'
+ features(1) = class'FixDoshSpam'
+ features(2) = class'FixFFHack'
+ features(3) = class'FixInfiniteNades'
+ features(4) = class'FixAmmoSelling'
+ features(5) = class'FixSpectatorCrash'
+ features(6) = class'FixDualiesCost'
+ features(7) = class'FixInventoryAbuse'
+ features(8) = class'FixProjectileFF'
+ features(9) = class'FixPipes'
+ features(10) = class'FixLogSpam'
}
\ No newline at end of file