diff --git a/sources/Gameplay/BaseClasses/BaseBackend.uc b/sources/Gameplay/BaseClasses/BaseBackend.uc
new file mode 100644
index 0000000..295a4d6
--- /dev/null
+++ b/sources/Gameplay/BaseClasses/BaseBackend.uc
@@ -0,0 +1,26 @@
+/**
+ * Base class for all backends. Does not define anything meaningful, which
+ * also means it does not put any limitations on it's implementation.
+ * 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 BaseBackend extends AcediaObject
+ abstract;
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Gameplay/BaseClasses/BaseFrontend.uc b/sources/Gameplay/BaseClasses/BaseFrontend.uc
new file mode 100644
index 0000000..83d3624
--- /dev/null
+++ b/sources/Gameplay/BaseClasses/BaseFrontend.uc
@@ -0,0 +1,26 @@
+/**
+ * Base class for all frontends. Does not define anything meaningful, which
+ * also means it does not put any limitations on it's implementation.
+ * 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 BaseFrontend extends AcediaObject
+ abstract;
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc b/sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc
new file mode 100644
index 0000000..90a490b
--- /dev/null
+++ b/sources/Gameplay/BaseClasses/KillingFloor/KFFrontend.uc
@@ -0,0 +1,41 @@
+/**
+ * Frontend skeleton for basic killing floor game mode.
+ * 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 KFFrontend extends BaseBackend
+ abstract;
+
+var private config class tradingClass;
+var public ATradingComponent trading;
+
+protected function Constructor()
+{
+ if (tradingClass != none) {
+ trading = ATradingComponent(_.memory.Allocate(tradingClass));
+ }
+}
+
+protected function Finalizer()
+{
+ _.memory.Free(trading);
+}
+
+defaultproperties
+{
+ tradingClass = none
+}
\ No newline at end of file
diff --git a/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATrader.uc b/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATrader.uc
new file mode 100644
index 0000000..c98d36c
--- /dev/null
+++ b/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATrader.uc
@@ -0,0 +1,196 @@
+/**
+ * Class, objects of which are expected to represent traders located on
+ * the map. In classic KF game mode it would represent areas behind closed
+ * doors that open during trader time and allow to purchase weapons and ammo.
+ * 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 ATrader extends AcediaObject
+ abstract;
+
+/**
+ * Returns location of the trader.
+ *
+ * Trader is usually associated with an area where players can trade and
+ * not just one point. Value returned by this method is merely expected to
+ * return position that "makes sense" for the trader.
+ * It can be used to calculate distance and/or path to the trader.
+ *
+ * @return Location of the caller trader.
+ */
+public function Vector GetLocation();
+
+/**
+ * Returns name of the trader.
+ *
+ * Trader name can be any non-empty `Text`.
+ * The only requirement is that after map's initialization every trader
+ * should have a unique name. It is not forbidden to break this invariant later
+ * by `SetName()` method.
+ * If `none` or empty name is passed, this method should do nothing.
+ *
+ * This is not the hard requirement, but explanation of purpose.
+ * Name does not have to be player-friendly, but it must be human-readable:
+ * it is not expected to be seen by regular players, but admins might use it
+ * to tweak their server.
+ *
+ * @return Current name of the trader.
+ */
+public function Text GetName();
+
+/**
+ * Changes name of the trader.
+ *
+ * @see `GetName()` for more details.
+ *
+ * @param newName New name of the caller trader.
+ * @return `true` if trader is currently enabled and `false` otherwise.
+ */
+public function ATrader SetName(Text newName);
+
+/**
+ * Checks if caller trader is currently enabled.
+ *
+ * Trader being enabled means that it can be opened and used for trading.
+ * Trader being disabled means that it cannot open for trading.
+ *
+ * This should override opened and auto-opened status.
+ *
+ * Marking disabled trader as selected is discouraged, especially for classic
+ * KF game mode, but should be allowed.
+ *
+ * @return `true` if trader is currently enabled and `false` otherwise.
+ */
+public function bool IsEnabled();
+
+/**
+ * Sets whether caller `ATrader`'s is currently enabled.
+ *
+ * Disabling the trader should automatically "boot" players out
+ * (see `BootPlayers()`).
+ *
+ * @see `IsEnabled()` for more info.
+ *
+ * @param doEnable `true` if trader is currently enabled and
+ * `false` otherwise.
+ * @return Caller `ATrader` to allow for method chaining.
+ */
+public function ATrader SetEnabled(bool doEnable);
+
+/**
+ * Checks whether caller `ATrader` will auto-open when trading gets activated.
+ *
+ * This setting must be ignored if trader is disabled, but disabling `ATrader`
+ * should not reset it.
+ *
+ * @return `true` if trader is marked to always auto-open upon activating
+ * trading (unless it is also disabled) and `false` otherwise.
+ */
+public function bool IsAutoOpen();
+
+/**
+ * Checks whether caller `ATrader` will auto-open when trading gets activated.
+ *
+ * @see `IsAutoOpen()` for more info.
+ *
+ * @param doAutoOpen `true` if trader should be marked to always auto-open
+ * upon activating trading and `false` otherwise.
+ * @return Caller `ATrader` to allow for method chaining.
+ */
+public function ATrader SetAutoOpen(bool doAutoOpen);
+
+/**
+ * Checks whether caller `ATrader` is currently open.
+ *
+ * `ATrader` being open means that players can "enter" (whatever that means for
+ * an implementation) and use `ATrader` to buy/sell equipment.
+ *
+ * @return `true` if it is open and `false` otherwise.
+ */
+public function bool IsOpen();
+
+/**
+ * Changes whether caller `ATrader` is open.
+ *
+ * Closing the trader should not automatically "boot" players out
+ * (see `BootPlayers()`).
+ *
+ * @see `IsOpen()` for more details.
+ *
+ * @param doOpen `true` if it is open and `false` otherwise.
+ * @return Caller `ATrader` to allow for method chaining.
+ */
+public function ATrader SetOpen(bool doOpen);
+
+/**
+ * Checks whether caller `ATrader` is currently marked as selected.
+ *
+ * @see `ATradingComponent.GetSelectedTrader()` for more details.
+ *
+ * @return `true` if caller `ATrader` is selected and `false` otherwise.
+ */
+public function bool IsSelected();
+
+/**
+ * Marks caller `ATrader` as a selected trader.
+ *
+ * @see `ATradingComponent.GetSelectedTrader()` for more details.
+ *
+ * @return Caller `ATrader` to allow for method chaining.
+ */
+public function ATrader Select();
+
+/**
+ * Removes players from the trader's place.
+ *
+ * In classic KF game mode it teleported them right outside the doors.
+ *
+ * This method's goal is to make sure players are not stuck in trader's place
+ * after it is closed. If that is impossible (for traders resembling
+ * KF2's one), then this method should do nothing.
+ *
+ * @return Caller `ATrader` to allow for method chaining.
+ */
+public function ATrader BootPlayers();
+
+/**
+ * Shortcut method to open the caller trader, guaranteed to be equivalent to
+ * `SetOpen(true)`. Provided for better interface.
+ *
+ * @return Caller `ATrader` to allow for method chaining.
+ */
+public final function ATrader Open()
+{
+ SetOpen(true);
+ return self;
+}
+
+/**
+ * Shortcut method to close the caller trader, guaranteed to be equivalent to
+ * `SetOpen(false)`. Provided for better interface.
+ *
+ * @return Caller `ATrader` to allow for method chaining.
+ */
+public final function ATrader Close()
+{
+ SetOpen(false);
+ return self;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc b/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc
new file mode 100644
index 0000000..87bc16c
--- /dev/null
+++ b/sources/Gameplay/BaseClasses/KillingFloor/Trading/ATradingComponent.uc
@@ -0,0 +1,204 @@
+/**
+ * Subset of functionality for dealing with everything related to traders.
+ * 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 ATradingComponent extends AcediaObject
+ abstract;
+
+var protected SimpleSignal onStartSignal;
+var protected SimpleSignal onEndSignal;
+var protected Trading_OnSelect_Signal onTraderSelectSignal;
+
+protected function Constructor()
+{
+ onStartSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
+ onEndSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
+ onTraderSelectSignal = Trading_OnSelect_Signal(
+ _.memory.Allocate(class'Trading_OnSelect_Signal'));
+}
+
+protected function Finalizer()
+{
+ _.memory.Free(onStartSignal);
+ _.memory.Free(onEndSignal);
+ _.memory.Free(onTraderSelectSignal);
+}
+
+/**
+ * Signal that will be emitted whenever trading time starts.
+ *
+ * [Signature]
+ * void ()
+ */
+/* SIGNAL */
+public final function SimpleSlot OnStart(AcediaObject receiver)
+{
+ return SimpleSlot(onStartSignal.NewSlot(receiver));
+}
+
+/**
+ * Signal that will be emitted whenever trading time ends.
+ *
+ * [Signature]
+ * void (ATrader oldTrader, ATrader newTrader)
+ *
+ * @param oldTrader Trader that was selected before this event.
+ * @param newTrader Trader that will be selected after this event.
+ */
+/* SIGNAL */
+public final function SimpleSlot OnEnd(AcediaObject receiver)
+{
+ return SimpleSlot(onEndSignal.NewSlot(receiver));
+}
+
+/**
+ * Signal that will be emitted whenever a new trader is selected.
+ *
+ * [Signature]
+ * void ()
+ */
+/* SIGNAL */
+public final function Trading_OnSelect_Slot OnTraderSelected(
+ AcediaObject receiver)
+{
+ return Trading_OnSelect_Slot(onTraderSelectSignal.NewSlot(receiver));
+}
+
+/**
+ * Returns array with all existing traders (including disabled once) on
+ * the level.
+ *
+ * @return Array of existing traders on the level. Guaranteed to not contain
+ * `none`-references. None of them should be deallocated,
+ * otherwise Acedia's behavior is undefined.
+ */
+public function array GetTraders();
+
+/**
+ * Checks whether trading is currently active.
+ *
+ * For classic KF game mode it means that it is trader time and one
+ * (or several) traders are open.
+ * This interface does not impose such limitation on trading: it is
+ * allowed to be active at any time, independent of anything else. However
+ * trading should only be permitted while trading is active.
+ *
+ * @return `true` if trading is active and `false` otherwise.
+ */
+public function bool IsTradingActive();
+
+/**
+ * Changes current status of trading.
+ *
+ * @see `IsTradingActive()` for more details.
+ */
+public function SetTradingStatus(bool makeActive);
+
+/**
+ * Returns the amount of time (in seconds) trading period will last for.
+ *
+ * For classic KF game mode it refers to how long trader time is
+ * (`60` seconds by default).
+ *
+ * @return Amount of time (in seconds) trading period will last for.
+ */
+public function int GetTradingInterval();
+
+/**
+ * Changes the amount of time (in seconds) trading period will last for.
+ *
+ * Changing this setting only affect current round (until the end of the map).
+ *
+ * For classic KF game mode it refers to how long trader time is
+ * (`60` seconds by default).
+ *
+ * @param newTradingInterval New length of the trading period.
+ */
+public function SetTradingInterval(int newTradingInterval);
+
+/**
+ * Return amount of time remaining in the current trading period.
+ *
+ * For classic KF game mode this refers to remaining trading time.
+ *
+ * @return Amount of time remaining in the current trading period.
+ * `0` if trading is currently inactive.
+ */
+public function int GetCountdown();
+
+/**
+ * Changes amount of time remaining in the current trading period.
+ *
+ * For classic KF game mode this refers to remaining trading time.
+ *
+ * @param newTradingInterval New amount of time that should remain in the
+ * current trading period. Values `<= 0` will lead to trading time ending
+ * immediately.
+ */
+public function SetCountdown(int newTradingInterval);
+
+/**
+ * Checks whether trading countdown was paused.
+ *
+ * Pause only affects current trading period and will be reset after
+ * the next starts.
+ *
+ * @return `true` if trading countdown was paused and `false` otherwise.
+ * If trading is inactive - returns `false`.
+ */
+public function bool IsCountDownPaused();
+
+/**
+ * Changes whether trading countdown should be paused.
+ *
+ * Pause set by this method only affects current trading period and will be
+ * reset after the next starts.
+ *
+ * @return doPause `true` to pause trading countdown and `false` to resume.
+ * If trading time is currently inactive - does nothing.
+ */
+public function SetCountDownPause(bool doPause);
+
+/**
+ * Returns currently selected trader.
+ *
+ * For classing KF game mode selected trader means the trader currently
+ * pointed at by the arrow in the top left corner on HUD and by the red wisp
+ * during trading time.
+ * This interface allows to generalize the concept of select trader to any
+ * specially marked trader or even not make use of it at all.
+ * Changing a selected trader in any way should always be followed
+ * by emitting `OnTraderSelected()` signal.
+ * After `SelectTrader()` call `GetSelectedTrader()` should return
+ * specified `ATrader`. If selected trader changes in some other way, it should
+ * first result in emitted `OnTraderSelected()` signal.
+ *
+ * @return Currently selected trader.
+ */
+public function ATrader GetSelectedTrader();
+
+/**
+ * Changes currently selected trader.
+ *
+ * @see `GetSelectedTrader()` for more details.
+ */
+public function SelectTrader(ATrader newSelection);
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Signal.uc b/sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Signal.uc
new file mode 100644
index 0000000..72a6efa
--- /dev/null
+++ b/sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Signal.uc
@@ -0,0 +1,39 @@
+/**
+ * Signal class implementation for `ATradingComponent`, for detecting when
+ * another trader is selected.
+ * 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 Trading_OnSelect_Signal extends Signal;
+
+public final function Emit(ATrader oldTrader, ATrader newTrader)
+{
+ local Slot nextSlot;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ Trading_OnSelect_Slot(nextSlot).connect(oldTrader, newTrader);
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'Trading_OnSelect_Slot'
+}
\ No newline at end of file
diff --git a/sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Slot.uc b/sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Slot.uc
new file mode 100644
index 0000000..4a625b0
--- /dev/null
+++ b/sources/Gameplay/BaseClasses/KillingFloor/Trading/Events/Trading_OnSelect_Slot.uc
@@ -0,0 +1,41 @@
+/**
+ * Slot class implementation for `ATradingComponent`'s signal for
+ * detecting when another trader is selected.
+ * 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 Trading_OnSelect_Slot extends Slot;
+
+delegate connect(ATrader oldTrader, ATrader newTrader)
+{
+ DummyCall();
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Gameplay/KF1Frontend/KF1_Frontend.uc b/sources/Gameplay/KF1Frontend/KF1_Frontend.uc
new file mode 100644
index 0000000..4200ded
--- /dev/null
+++ b/sources/Gameplay/KF1Frontend/KF1_Frontend.uc
@@ -0,0 +1,27 @@
+/**
+ * Frontend implementation for classic `KFGameType` that changes as little as
+ * possible and only on request from another mod, otherwise not altering
+ * gameplay at all.
+ * 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 KF1_Frontend extends KFFrontend;
+
+defaultproperties
+{
+ tradingClass = class'KF1_TradingComponent'
+}
\ No newline at end of file
diff --git a/sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc b/sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc
new file mode 100644
index 0000000..a141838
--- /dev/null
+++ b/sources/Gameplay/KF1Frontend/Trading/KF1_Trader.uc
@@ -0,0 +1,248 @@
+/**
+ * `ATrader`'s implementation for `KF1_Frontend`.
+ * Wrapper for KF1's `ShopVolume`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 .
+ */
+class KF1_Trader extends ATrader;
+
+// We do not use any vanilla value as a name, instead storing and tracking it
+// entirely as our own value.
+var protected Text myName;
+// Reference to `ShopVolume` actor that this `KF1_Trader` represents.
+var protected NativeActorRef myShopVolume;
+
+protected function Finalizer()
+{
+ _.memory.Free(myShopVolume);
+ myShopVolume = none;
+}
+
+/**
+ * Detect all existing traders on the level and created a `KF1_Trader` for
+ * each of them.
+ *
+ * @return Array of created `KF1_Trader`s. All of them are guaranteed to not
+ * be `none`.
+ */
+public static function array WrapVanillaShops()
+{
+ local int shopCounter;
+ local MutableText textBuilder;
+ local LevelInfo level;
+ local KFGameType kfGame;
+ local KF1_Trader nextTrader;
+ local array allTraders;
+ local ShopVolume nextShopVolume;
+ level = __().unreal.GetLevel();
+ kfGame = __().unreal.GetKFGameType();
+ textBuilder = __().text.Empty();
+ foreach level.AllActors(class'ShopVolume', nextShopVolume)
+ {
+ if (nextShopVolume == none) continue;
+ if (!nextShopVolume.bObjectiveModeOnly || kfGame.bUsingObjectiveMode)
+ {
+ nextTrader = KF1_Trader(__().memory.Allocate(class'KF1_Trader'));
+ nextTrader.myShopVolume = __().unreal.ActorRef(nextShopVolume);
+ textBuilder.Clear().AppendPlainString("trader" $ shopCounter);
+ nextTrader.myName = textBuilder.Copy();
+ allTraders[allTraders.length] = nextTrader;
+ shopCounter += 1;
+ }
+ }
+ textBuilder.FreeSelf();
+ return allTraders;
+}
+
+public function Text GetName()
+{
+ if (myName == none) {
+ return _.text.Empty();
+ }
+ return myName.Copy();
+}
+
+public function ATrader SetName(Text newName)
+{
+ if (newName == none) return self;
+ if (newName.IsEmpty()) return self;
+
+ myName.FreeSelf();
+ newName = newName.Copy();
+ return self;
+}
+
+public function Vector GetLocation()
+{
+ local ShopVolume vanillaShopVolume;
+ vanillaShopVolume = ShopVolume(myShopVolume.Get());
+ if (vanillaShopVolume != none) {
+ return vanillaShopVolume.location;
+ }
+ return Vect(0, 0, 0);
+}
+
+public function bool IsEnabled()
+{
+ local ShopVolume vanillaShopVolume;
+ vanillaShopVolume = ShopVolume(myShopVolume.Get());
+ if (vanillaShopVolume != none) {
+ return !vanillaShopVolume.bAlwaysClosed;
+ }
+ return false;
+}
+
+public function ATrader SetEnabled(bool doEnable)
+{
+ local ShopVolume vanillaShopVolume;
+ vanillaShopVolume = ShopVolume(myShopVolume.Get());
+ if (vanillaShopVolume == none) {
+ return self;
+ }
+ if (doEnable) {
+ vanillaShopVolume.bAlwaysClosed = false;
+ }
+ else
+ {
+ vanillaShopVolume.bAlwaysClosed = true;
+ Close();
+ BootPlayers();
+ }
+ UpdateShopList();
+ return self;
+}
+
+/**
+ * This method re-fills `KFGameType.shopList` to contain only currently
+ * enabled traders.
+ */
+protected function UpdateShopList()
+{
+ local int i;
+ local ShopVolume nextShopVolume;
+ local KF1_Trader nextTrader;
+ local array shopVolumes;
+ local array availableTraders;
+ availableTraders = _.kf.trading.GetTraders();
+ for (i = 0; i < availableTraders.length; i += 1)
+ {
+ nextTrader = KF1_Trader(availableTraders[i]);
+ if (nextTrader == none) continue;
+ if (!nextTrader.IsEnabled()) continue;
+ nextShopVolume = ShopVolume(nextTrader.myShopVolume.Get());
+ if (nextShopVolume == none) continue;
+
+ shopVolumes[shopVolumes.length] = nextShopVolume;
+ }
+ _.unreal.GetKFGameType().shopList = shopVolumes;
+}
+
+public function bool IsAutoOpen()
+{
+ local ShopVolume vanillaShopVolume;
+ vanillaShopVolume = ShopVolume(myShopVolume.Get());
+ if (vanillaShopVolume != none) {
+ return vanillaShopVolume.bAlwaysEnabled;
+ }
+ return false;
+}
+
+public function ATrader SetAutoOpen(bool doAutoOpen)
+{
+ local ShopVolume vanillaShopVolume;
+ vanillaShopVolume = ShopVolume(myShopVolume.Get());
+ if (vanillaShopVolume == none) {
+ return self;
+ }
+ if (doAutoOpen) {
+ vanillaShopVolume.bAlwaysEnabled = true;
+ }
+ else {
+ vanillaShopVolume.bAlwaysEnabled = false;
+ }
+ return self;
+}
+
+public function bool IsOpen()
+{
+ local ShopVolume vanillaShopVolume;
+ vanillaShopVolume = ShopVolume(myShopVolume.Get());
+ if (vanillaShopVolume != none) {
+ return vanillaShopVolume.bCurrentlyOpen;
+ }
+ return false;
+}
+
+public function ATrader SetOpen(bool doOpen)
+{
+ local ShopVolume vanillaShopVolume;
+ if (doOpen && !IsEnabled()) return self;
+ vanillaShopVolume = ShopVolume(myShopVolume.Get());
+ if (vanillaShopVolume == none) return self;
+
+ if (doOpen) {
+ vanillaShopVolume.OpenShop();
+ }
+ else {
+ vanillaShopVolume.CloseShop();
+ }
+ return self;
+}
+
+public function bool IsSelected()
+{
+ local ShopVolume vanillaShopVolume;
+ local KFGameReplicationInfo kfGameRI;
+ vanillaShopVolume = ShopVolume(myShopVolume.Get());
+ if (vanillaShopVolume == none) {
+ return false;
+ }
+ kfGameRI = _.unreal.GetKFGameRI();
+ if (kfGameRI != none) {
+ return (kfGameRI.currentShop == vanillaShopVolume);
+ }
+ return false;
+}
+
+public function ATrader Select()
+{
+ local ShopVolume vanillaShopVolume;
+ local KFGameReplicationInfo kfGameRI;
+ vanillaShopVolume = ShopVolume(myShopVolume.Get());
+ if (vanillaShopVolume == none) {
+ return self;
+ }
+ kfGameRI = _.unreal.GetKFGameRI();
+ if (kfGameRI != none) {
+ kfGameRI.currentShop = vanillaShopVolume;
+ }
+ return self;
+}
+
+public function ATrader BootPlayers()
+{
+ local ShopVolume vanillaShopVolume;
+ vanillaShopVolume = ShopVolume(myShopVolume.Get());
+ if (vanillaShopVolume != none) {
+ vanillaShopVolume.BootPlayers();
+ }
+ return self;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc b/sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc
new file mode 100644
index 0000000..434b14b
--- /dev/null
+++ b/sources/Gameplay/KF1Frontend/Trading/KF1_TradingComponent.uc
@@ -0,0 +1,213 @@
+/**
+ * `ATradingComponent`'s implementation for `KF1_Frontend`.
+ * Only supports `KF1_Trader` as a possible trader class.
+ * 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 KF1_TradingComponent extends ATradingComponent;
+
+// Variables for enforcing a trader time pause by repeatedly setting
+// `waveCountDown`'s value to `pausedCountDownValue`
+var protected bool tradingCountDownPaused;
+var protected int pausedCountDownValue;
+
+// For detecting events of trading becoming active/inactive and selecting
+// a different trader, to account for these changing through non-Acedia means
+var protected bool wasActiveLastCheck;
+var protected Atrader lastSelectedTrader;
+
+// All known traders on map
+var protected array registeredTraders;
+
+protected function Constructor()
+{
+ super.Constructor();
+ _.unreal.OnTick(self).connect = Tick;
+ registeredTraders = class'KF1_Trader'.static.WrapVanillaShops();
+ lastSelectedTrader = GetSelectedTrader();
+ wasActiveLastCheck = IsTradingActive();
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ _.unreal.OnTick(self).Disconnect();
+ _.memory.FreeMany(registeredTraders);
+ registeredTraders.length = 0;
+}
+
+public function array GetTraders()
+{
+ return registeredTraders;
+}
+
+public function bool IsTradingActive()
+{
+ local KFGameType kfGame;
+ kfGame = _.unreal.GetKFGameType();
+ return kfGame.IsInState('MatchInProgress') && kfGame.bTradingDoorsOpen;
+}
+
+public function SetTradingStatus(bool makeActive)
+{
+ local bool isCurrentlyActive;
+ local KFGameType kfGame;
+ local KFGameReplicationInfo kfGameRI;
+ local KFMonster nextZed;
+ isCurrentlyActive = IsTradingActive();
+ if (isCurrentlyActive == makeActive) {
+ return;
+ }
+ if (!makeActive && isCurrentlyActive)
+ {
+ SetCountDown(0);
+ return;
+ }
+ kfGame = _.unreal.GetKFGameType();
+ kfGameRI = _.unreal.GetKFGameRI();
+ foreach kfGame.DynamicActors(class'KFMonster', nextZed)
+ {
+ if (nextZed == none) continue;
+ if (nextZed.health <= 0) continue;
+ nextZed.Suicide();
+ }
+ kfGame.totalMaxMonsters = 0;
+ kfGameRI.maxMonsters = 0;
+}
+
+public function ATrader GetSelectedTrader()
+{
+ local int i;
+ for (i = 0; i < registeredTraders.length; i += 1)
+ {
+ if (registeredTraders[i].IsSelected()) {
+ return registeredTraders[i];
+ }
+ }
+ return none;
+}
+
+public function SelectTrader(ATrader newSelection)
+{
+ local ATrader oldSelection;
+ local KFGameReplicationInfo kfGameRI;
+ if (newSelection != none) {
+ newSelection.Select();
+ }
+ else
+ {
+ kfGameRI = _.unreal.GetKFGameRI();
+ if (kfGameRI != none) {
+ kfGameRI.currentShop = none;
+ }
+ }
+ // Emit signal, but first record new trader inside `lastSelectedTrader`
+ // in case someone decides it would be a grand idea to call `SelectTrader`
+ // during `onTraderSelectSignal` signal.
+ oldSelection = lastSelectedTrader;
+ lastSelectedTrader = newSelection;
+ if (lastSelectedTrader != newSelection) {
+ onTraderSelectSignal.Emit(oldSelection, newSelection);
+ }
+}
+
+public function int GetTradingInterval()
+{
+ return _.unreal.GetKFGameType().timeBetweenWaves;
+}
+
+public function SetTradingInterval(int newTradingInterval)
+{
+ if (newTradingInterval > 0) {
+ _.unreal.GetKFGameType().timeBetweenWaves = Max(newTradingInterval, 1);
+ }
+}
+
+public function int GetCountDown()
+{
+ if (!IsTradingActive()) {
+ return 0;
+ }
+ return _.unreal.GetKFGameType().waveCountDown;
+}
+
+public function SetCountDown(int newCountDownValue)
+{
+ local KFGameType kfGame;
+ if (!IsTradingActive()) {
+ return;
+ }
+ kfGame = _.unreal.GetKFGameType();
+ if (kfGame.waveCountDown >= 5 && newCountDownValue < 5) {
+ _.unreal.GetKFGameRI().waveNumber = kfGame.waveNum;
+ }
+ kfGame.waveCountDown = Max(newCountDownValue, 1);
+ pausedCountDownValue = newCountDownValue;
+}
+
+public function bool IsCountDownPaused()
+{
+ if (!IsTradingActive()) {
+ return false;
+ }
+ return tradingCountDownPaused;
+}
+
+public function SetCountDownPause(bool doPause)
+{
+ tradingCountDownPaused = doPause;
+ if (doPause) {
+ pausedCountDownValue = _.unreal.GetKFGameType().waveCountDown;
+ }
+}
+
+protected function Tick(float delta, float timeScaleCoefficient)
+{
+ local bool isActiveNow;
+ local ATrader newSelectedTrader;
+ // Enforce pause
+ if (tradingCountDownPaused) {
+ _.unreal.GetKFGameType().waveCountDown = pausedCountDownValue;
+ }
+ // Selected trader check
+ newSelectedTrader = GetSelectedTrader();
+ if (lastSelectedTrader != newSelectedTrader)
+ {
+ onTraderSelectSignal.Emit(lastSelectedTrader, newSelectedTrader);
+ lastSelectedTrader = newSelectedTrader;
+ }
+ // Active status check
+ isActiveNow = IsTradingActive();
+ if (wasActiveLastCheck != isActiveNow)
+ {
+ wasActiveLastCheck = isActiveNow;
+ if (isActiveNow)
+ {
+ onStartSignal.Emit();
+ }
+ else
+ {
+ onEndSignal.Emit();
+ // Reset pause after trading time has ended
+ tradingCountDownPaused = false;
+ }
+ }
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Global.uc b/sources/Global.uc
index 0e046d4..9a7de61 100644
--- a/sources/Global.uc
+++ b/sources/Global.uc
@@ -40,6 +40,8 @@ var public UserAPI users;
var public PlayersAPI players;
var public JSONAPI json;
+var public KFFrontend kf;
+
public final static function Global GetInstance()
{
if (default.myself == none) {
@@ -70,5 +72,6 @@ protected function Initialize()
users = UserAPI(memory.Allocate(class'UserAPI'));
players = PlayersAPI(memory.Allocate(class'PlayersAPI'));
json = JSONAPI(memory.Allocate(class'JSONAPI'));
+ kf = KFFrontend(memory.Allocate(class'KF1_Frontend'));
json.StaticConstructor();
}
\ No newline at end of file