diff --git a/sources/Unreal/Events/Unreal_OnTick_Signal.uc b/sources/Unreal/Events/Unreal_OnTick_Signal.uc
new file mode 100644
index 0000000..62cdbbe
--- /dev/null
+++ b/sources/Unreal/Events/Unreal_OnTick_Signal.uc
@@ -0,0 +1,38 @@
+/**
+ * Signal class implementation for `UnrealAPI`'s `OnTick` signal.
+ * 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 Unreal_OnTick_Signal extends Signal;
+
+public final function Emit(float delta, float dilationCoefficient)
+{
+ local Slot nextSlot;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ Unreal_OnTick_Slot(nextSlot).connect(delta, dilationCoefficient);
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'Unreal_OnTick_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/Events/Unreal_OnTick_Slot.uc b/sources/Unreal/Events/Unreal_OnTick_Slot.uc
new file mode 100644
index 0000000..3a4ad96
--- /dev/null
+++ b/sources/Unreal/Events/Unreal_OnTick_Slot.uc
@@ -0,0 +1,40 @@
+/**
+ * Slot class implementation for `UnrealAPI`'s `OnTick` signal.
+ * 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 Unreal_OnTick_Slot extends Slot;
+
+delegate connect(float delta, float dilationCoefficient)
+{
+ DummyCall();
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRules/AcediaGameRules.uc b/sources/Unreal/GameRules/AcediaGameRules.uc
new file mode 100644
index 0000000..8552e9f
--- /dev/null
+++ b/sources/Unreal/GameRules/AcediaGameRules.uc
@@ -0,0 +1,109 @@
+/**
+ * Acedia's `GameRules` class that provides `GameRules`'s events through
+ * the signal/slot functionality.
+ * 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 AcediaGameRules extends GameRules;
+
+var private GameRules_OnFindPlayerStart_Signal onFindPlayerStartSignal;
+var private GameRules_OnOverridePickupQuery_Signal onOverridePickupQuery;
+var private GameRules_OnNetDamage_Signal onNetDamage;
+
+public final function Initialize(unrealService service)
+{
+ if (service == none) {
+ return;
+ }
+ onFindPlayerStartSignal = GameRules_OnFindPlayerStart_Signal(
+ service.GetSignal(class'GameRules_OnFindPlayerStart_Signal'));
+ onOverridePickupQuery = GameRules_OnOverridePickupQuery_Signal(
+ service.GetSignal(class'GameRules_OnOverridePickupQuery_Signal'));
+ onNetDamage = GameRules_OnNetDamage_Signal(
+ service.GetSignal(class'GameRules_OnNetDamage_Signal'));
+}
+
+function string GetRules()
+{
+ local string resultSet;
+ resultSet = "acedia";
+ if (nextGameRules != none) {
+ resultSet = resultSet $ nextGameRules.GetRules();
+ }
+ return resultSet;
+}
+
+function NavigationPoint FindPlayerStart(
+ Controller player,
+ optional byte inTeam,
+ optional string incomingName)
+{
+ local NavigationPoint result;
+ if (onFindPlayerStartSignal != none) {
+ result = onFindPlayerStartSignal.Emit(player, inTeam, incomingName);
+ }
+ if (result == none && nextGameRules != none) {
+ return nextGameRules.FindPlayerStart(player, inTeam, incomingName);
+ }
+ return result;
+}
+
+function bool OverridePickupQuery(
+ Pawn other,
+ Pickup item,
+ out byte allowPickup)
+{
+ local bool shouldOverride;
+ if (onOverridePickupQuery != none) {
+ shouldOverride = onOverridePickupQuery.Emit(other, item, allowPickup);
+ }
+ if (shouldOverride) {
+ return true;
+ }
+ if (nextGameRules != none) {
+ return nextGameRules.OverridePickupQuery(other, item, allowPickup);
+ }
+ return false;
+}
+
+function int NetDamage(
+ int originalDamage,
+ int damage,
+ Pawn injured,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ out Vector momentum,
+ class damageType)
+{
+ if (onNetDamage != none)
+ {
+ damage = onNetDamage.Emit( originalDamage, damage, injured,
+ instigatedBy, hitLocation, momentum,
+ damageType);
+ }
+ if (nextGameRules != none)
+ {
+ return nextGameRules.NetDamage( originalDamage, damage, injured,
+ instigatedBy, hitLocation, momentum,
+ damageType);
+ }
+ return damage;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Signal.uc b/sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Signal.uc
new file mode 100644
index 0000000..61cba42
--- /dev/null
+++ b/sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Signal.uc
@@ -0,0 +1,48 @@
+/**
+ * Signal class implementation for `GameRulesAPI`'s `OnFindPlayerStart` signal.
+ * 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 GameRules_OnFindPlayerStart_Signal extends Signal;
+
+public final function NavigationPoint Emit(
+ Controller player,
+ optional byte inTeam,
+ optional string incomingName
+)
+{
+ local Slot nextSlot;
+ local NavigationPoint nextPoint;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ nextPoint = GameRules_OnFindPlayerStart_Slot(nextSlot)
+ .connect(player, inTeam, incomingName);
+ if (nextPoint != none && !nextSlot.IsEmpty()) {
+ return nextPoint;
+ }
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+ return none;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'GameRules_OnFindPlayerStart_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Slot.uc b/sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Slot.uc
new file mode 100644
index 0000000..2f13983
--- /dev/null
+++ b/sources/Unreal/GameRules/Events/GameRules_OnFindPlayerStart_Slot.uc
@@ -0,0 +1,44 @@
+/**
+ * Slot class implementation for `GameRulesAPI`'s `OnFindPlayerStart` signal.
+ * 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 GameRules_OnFindPlayerStart_Slot extends Slot;
+
+delegate NavigationPoint connect(
+ Controller player,
+ optional byte inTeam,
+ optional string incomingName)
+{
+ DummyCall();
+ return none;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Signal.uc b/sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Signal.uc
new file mode 100644
index 0000000..d921073
--- /dev/null
+++ b/sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Signal.uc
@@ -0,0 +1,52 @@
+/**
+ * Signal class implementation for `GameRulesAPI`'s `OnNetDamage` signal.
+ * 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 GameRules_OnNetDamage_Signal extends Signal;
+
+public final function int Emit(
+ int originalDamage,
+ int damage,
+ Pawn injured,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ out Vector momentum,
+ class damageType)
+{
+ local Slot nextSlot;
+ local int newDamage;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ newDamage = GameRules_OnNetDamage_Slot(nextSlot)
+ .connect( originalDamage, damage, injured, instigatedBy,
+ hitLocation, momentum, damageType);
+ if (!nextSlot.IsEmpty()) {
+ damage = newDamage;
+ }
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+ return damage;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'GameRules_OnNetDamage_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Slot.uc b/sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Slot.uc
new file mode 100644
index 0000000..97d86aa
--- /dev/null
+++ b/sources/Unreal/GameRules/Events/GameRules_OnNetDamage_Slot.uc
@@ -0,0 +1,49 @@
+/**
+ * Slot class implementation for `GameRulesAPI`'s `OnNetDamage` signal.
+ * 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 GameRules_OnNetDamage_Slot extends Slot;
+
+delegate int connect(
+ int originalDamage,
+ int damage,
+ Pawn injured,
+ Pawn instigatedBy,
+ Vector hitLocation,
+ out Vector momentum,
+ class damageType)
+{
+ DummyCall();
+ // Do not alter any already made modifications
+ return damage;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Signal.uc b/sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Signal.uc
new file mode 100644
index 0000000..ff54155
--- /dev/null
+++ b/sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Signal.uc
@@ -0,0 +1,45 @@
+/**
+ * Signal class implementation for `GameRulesAPI`'s
+ * `OnOverridePickupQuerySignal` signal.
+ * 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 GameRules_OnOverridePickupQuery_Signal extends Signal;
+
+public final function bool Emit(Pawn other, Pickup item, out byte allowPickup)
+{
+ local Slot nextSlot;
+ local bool shouldOverride;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ shouldOverride = GameRules_OnOverridePickupQuery_Slot(nextSlot)
+ .connect(other, item, allowPickup);
+ if (shouldOverride && !nextSlot.IsEmpty()) {
+ return shouldOverride;
+ }
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+ return false;
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'GameRules_OnOverridePickupQuery_Slot'
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Slot.uc b/sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Slot.uc
new file mode 100644
index 0000000..af1a12e
--- /dev/null
+++ b/sources/Unreal/GameRules/Events/GameRules_OnOverridePickupQuery_Slot.uc
@@ -0,0 +1,46 @@
+/**
+ * Slot class implementation for `GameRulesAPI`'s
+ * `OnOverridePickupQuerySignal` signal.
+ * 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 GameRules_OnOverridePickupQuery_Slot extends Slot;
+
+delegate bool connect(
+ Pawn other,
+ Pickup item,
+ out byte allowPickup)
+{
+ DummyCall();
+ // Do not override pickup queue by default
+ return false;
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Unreal/GameRules/GameRulesAPI.uc b/sources/Unreal/GameRules/GameRulesAPI.uc
new file mode 100644
index 0000000..6b19cfb
--- /dev/null
+++ b/sources/Unreal/GameRules/GameRulesAPI.uc
@@ -0,0 +1,232 @@
+/**
+ * Low-level API that provides set of utility methods for working with
+ * `GameRule`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 GameRulesAPI extends AcediaObject;
+
+var private LoggerAPI.Definition errNoService;
+
+/**
+ * Called when game decides on a player's spawn point. If a `NavigationPoint`
+ * is returned, signal propagation will be interrupted and returned value will
+ * be used as the player start.
+ *
+ * [Signature]
+ * NavigationPoint (
+ * Controller player,
+ * optional byte inTeam,
+ * optional string incomingName)
+ *
+ * @param player Player for whom we are picking a spawn point.
+ * @param inTeam Player's team number.
+ * @param incomingName `Portal` parameter from `GameInfo.Login()` event.
+ * @return `NavigationPoint` that will player must be spawned at.
+ * `none` means that slot does not want to modify it.
+ */
+/* SIGNAL */
+public final function GameRules_OnFindPlayerStart_Slot OnFindPlayerStart(
+ AcediaObject receiver)
+{
+ local Signal signal;
+ local UnrealService service;
+ service = UnrealService(class'UnrealService'.static.Require());
+ if (service == none)
+ {
+ _.logger.Auto(errNoService);
+ return none;
+ }
+ signal = service.GetSignal(class'GameRules_OnFindPlayerStart_Signal');
+ return GameRules_OnFindPlayerStart_Slot(signal.NewSlot(receiver));
+}
+
+/**
+ * When pawn wants to pickup something, `GameRule`s are given a chance to
+ * modify it. If one of the `Slot`s returns `true`, `allowPickup` will
+ * determine if the object can be picked up.
+ * Overriding via this method allows to completely bypass check against
+ * `Pawn`'s inventory's `HandlePickupQuery()` method.
+ *
+ * [Signature]
+ * bool (Pawn other, Pickup item, out byte allowPickup)
+ *
+ * @param other Pawn which will potentially pickup `item`.
+ * @param item Pickup which `other` might potentially pickup.
+ * @param allowPickup `true` if you want to force `other` to pickup an item
+ * and `false` otherwise. This parameter is ignored if returned value of
+ * your slot call is `false`.
+ * @return `true` if you wish to override decision about pickup with
+ * `allowPickup` and `false` if you do not want to make that decision.
+ * If you do decide to override decision by returning `true` - this signal
+ * will not be propagated to the rest of the slots.
+ */
+/* SIGNAL */
+public final function GameRules_OnOverridePickupQuery_Slot
+ OnOverridePickupQuery(AcediaObject receiver)
+{
+ local Signal signal;
+ local UnrealService service;
+ service = UnrealService(class'UnrealService'.static.Require());
+ if (service == none)
+ {
+ _.logger.Auto(errNoService);
+ return none;
+ }
+ signal = service.GetSignal(class'GameRules_OnOverridePickupQuery_Signal');
+ return GameRules_OnOverridePickupQuery_Slot(signal.NewSlot(receiver));
+}
+
+// TODO: rewrite
+/**
+ * When pawn wants to pickup something, `GameRule`s are given a chance to
+ * modify it. If one of the `Slot`s returns `true`, `allowPickup` will
+ * determine if the object can be picked up.
+ * Overriding via this method allows to completely bypass check against
+ * `Pawn`'s inventory's `HandlePickupQuery()` method.
+ *
+ * [Signature]
+ * bool (Pawn other, Pickup item, out byte allowPickup)
+ *
+ * @param other Pawn which will potentially pickup `item`.
+ * @param item Pickup which `other` might potentially pickup.
+ * @param allowPickup `true` if you want to force `other` to pickup an item
+ * and `false` otherwise. This parameter is ignored if returned value of
+ * your slot call is `false`.
+ * @return `true` if you wish to override decision about pickup with
+ * `allowPickup` and `false` if you do not want to make that decision.
+ * If you do decide to override decision by returning `true` - this signal
+ * will not be propagated to the rest of the slots.
+ */
+/* SIGNAL */
+public final function GameRules_OnNetDamage_Slot OnNetDamage(
+ AcediaObject receiver)
+{
+ local Signal signal;
+ local UnrealService service;
+ service = UnrealService(class'UnrealService'.static.Require());
+ if (service == none)
+ {
+ _.logger.Auto(errNoService);
+ return none;
+ }
+ signal = service.GetSignal(class'GameRules_OnNetDamage_Signal');
+ return GameRules_OnNetDamage_Slot(signal.NewSlot(receiver));
+}
+
+/**
+ * Adds new `GameRules` class to the current `GameInfo`.
+ * Does nothing if give `GameRules` class was already added before.
+ *
+ * @param newRulesClass Class of rules to add.
+ * @return `true` if `GameRules` were added and `false` otherwise
+ * (because they were already active.)
+ */
+public final function bool Add(class newRulesClass)
+{
+ if (AreAdded(newRulesClass)) {
+ return false;
+ }
+ _.unreal.GetGameType()
+ .AddGameModifier(GameRules(_.memory.Allocate(newRulesClass)));
+ return true;
+}
+
+/**
+ * Removes given `GameRules` class from the current `GameInfo`,
+ * if they are active. Does nothing otherwise.
+ *
+ * @param rulesClassToRemove Class of rules to try and remove.
+ * @return `true` if `GameRules` were removed and `false` otherwise
+ * (if they were not active in the first place).
+ */
+public final function bool Remove(class rulesClassToRemove)
+{
+ local GameInfo game;
+ local GameRules rulesIter;
+ local GameRules rulesToDestroy;
+ if (rulesClassToRemove == none) return false;
+ game = _.unreal.GetGameType();
+ if (game.gameRulesModifiers == none) return false;
+
+ // Check root rules
+ rulesToDestroy = game.gameRulesModifiers;
+ if (rulesToDestroy.class == rulesClassToRemove)
+ {
+ game.gameRulesModifiers = rulesToDestroy.nextGameRules;
+ rulesToDestroy.Destroy();
+ return true;
+ }
+ // Check rest of the rules
+ rulesIter = game.gameRulesModifiers;
+ while (rulesIter != none)
+ {
+ rulesToDestroy = rulesIter.nextGameRules;
+ if ( rulesToDestroy != none
+ && rulesToDestroy.class == rulesClassToRemove)
+ {
+ rulesIter.nextGameRules = rulesToDestroy.nextGameRules;
+ rulesToDestroy.Destroy();
+ return true;
+ }
+ rulesIter = rulesIter.nextGameRules;
+ }
+ return false;
+}
+
+/**
+ * Finds given class of `GameRules` if it's currently active in `GameInfo`.
+ * Returns `none` otherwise.
+ *
+ * @param rulesClassToFind Class of rules to find.
+ * @return `GameRules` of given class `rulesClassToFind` instance added to
+ * `GameInfo`'s records and `none` if no such rules are currently added.
+ */
+public final function GameRules FindInstance(
+ class rulesClassToFind)
+{
+ local GameRules rulesIter;
+ if (rulesClassToFind == none) {
+ return none;
+ }
+ rulesIter = _.unreal.GetGameType().gameRulesModifiers;
+ while (rulesIter != none)
+ {
+ if (rulesIter.class == rulesClassToFind) {
+ return rulesIter;
+ }
+ rulesIter = rulesIter.nextGameRules;
+ }
+ return none;
+}
+
+/**
+ * Checks if given class of `GameRules` is currently active in `GameInfo`.
+ *
+ * @param rulesClassToCheck Class of rules to check for.
+ * @return `true` if `GameRules` are active and `false` otherwise.
+ */
+public final function bool AreAdded(
+ class rulesClassToCheck)
+{
+ return (FindInstance(rulesClassToCheck) != none);
+}
+
+defaultproperties
+{
+ errNoService = (l=LOG_Error,m="`UnrealService` could not be reached.")
+}
\ No newline at end of file
diff --git a/sources/Unreal/Tests/TEST_UnrealAPI.uc b/sources/Unreal/Tests/TEST_UnrealAPI.uc
index a3fa8b5..d4db865 100644
--- a/sources/Unreal/Tests/TEST_UnrealAPI.uc
+++ b/sources/Unreal/Tests/TEST_UnrealAPI.uc
@@ -19,10 +19,10 @@
*/
class TEST_UnrealAPI extends TestCase;
-protected static function int CountRulesAmount(class gameRulesClass)
+protected static function int CountRulesAmount(class gameRulesClass)
{
local int counter;
- local GameRules rulesIter;
+ local gameRules rulesIter;
if (gameRulesClass == none) {
return 0;
}
@@ -32,7 +32,7 @@ protected static function int CountRulesAmount(class gameRulesClass)
if (rulesIter.class == gameRulesClass) {
counter += 1;
}
- rulesIter = rulesIter.nextGameRules;
+ rulesIter = rulesIter.nextgameRules;
}
return counter;
}
@@ -57,38 +57,38 @@ protected static function Test_GameType()
protected static function Test_GameRules()
{
- Context("Testing methods for working with `GameRules`.");
+ Context("Testing methods for working with `gameRules`.");
SubTest_AddRemoveGameRules();
SubTest_CheckGameRules();
}
protected static function SubTest_AddRemoveGameRules()
{
- Issue("`AddGameRules()` does not add game rules.");
- __().unreal.AddGameRules(class'MockGameRulesA');
+ Issue("`gameRules.Add()` does not add game rules.");
+ __().unreal.gameRules.Add(class'MockGameRulesA');
TEST_ExpectTrue(CountRulesAmount(class'MockGameRulesA') == 1);
- __().unreal.AddGameRules(class'MockGameRulesA');
- Issue("Calling `AddGameRules()` twice leads to rule duplication.");
+ __().unreal.gameRules.Add(class'MockGameRulesA');
+ Issue("Calling `gameRules.Add()` twice leads to rule duplication.");
TEST_ExpectFalse(CountRulesAmount(class'MockGameRulesA') > 1);
- Issue("Calling `AddGameRules()` leads to rule not being added.");
+ Issue("Calling `gameRules.Add()` leads to rule not being added.");
TEST_ExpectFalse(CountRulesAmount(class'MockGameRulesA') == 0);
- Issue("Adding new rules with `AddGameRules()` does not work properly.");
- __().unreal.AddGameRules(class'MockGameRulesB');
+ Issue("Adding new rules with `gameRules.Add()` does not work properly.");
+ __().unreal.gameRules.Add(class'MockGameRulesB');
TEST_ExpectTrue(CountRulesAmount(class'MockGameRulesA') == 1);
TEST_ExpectTrue(CountRulesAmount(class'MockGameRulesB') == 1);
- Issue("Adding/removing rules with `RemoveGameRules()` leads to" @
+ Issue("Adding/removing rules with `gameRules.Remove()` leads to" @
"unexpected results.");
- __().unreal.RemoveGameRules(class'MockGameRulesB');
+ __().unreal.gameRules.Remove(class'MockGameRulesB');
TEST_ExpectTrue(CountRulesAmount(class'MockGameRulesA') == 1);
TEST_ExpectTrue(CountRulesAmount(class'MockGameRulesB') == 0);
- __().unreal.AddGameRules(class'MockGameRulesB');
- __().unreal.RemoveGameRules(class'MockGameRulesA');
+ __().unreal.gameRules.Add(class'MockGameRulesB');
+ __().unreal.gameRules.Remove(class'MockGameRulesA');
TEST_ExpectTrue(CountRulesAmount(class'MockGameRulesA') == 0);
TEST_ExpectTrue(CountRulesAmount(class'MockGameRulesB') == 1);
- __().unreal.RemoveGameRules(class'MockGameRulesB');
+ __().unreal.gameRules.Remove(class'MockGameRulesB');
TEST_ExpectTrue(CountRulesAmount(class'MockGameRulesA') == 0);
TEST_ExpectTrue(CountRulesAmount(class'MockGameRulesB') == 0);
}
@@ -96,32 +96,32 @@ protected static function SubTest_AddRemoveGameRules()
protected static function SubTest_CheckGameRules()
{
local string issueForAdded, issueForNotAdded;
- issueForAdded = "`AreGameRulesAdded()` returns `false` for rules that are" @
- "currently added.";
- issueForNotAdded = "`AreGameRulesAdded()` returns `true` for rules that" @
+ issueForAdded = "`gameRules.AreAdded()` returns `false` for rules that"
+ @ "are currently added.";
+ issueForNotAdded = "`gameRules.AreAdded()` returns `true` for rules that" @
"are not currently added.";
- __().unreal.RemoveGameRules(class'MockGameRulesA');
- __().unreal.RemoveGameRules(class'MockGameRulesB');
+ __().unreal.gameRules.Remove(class'MockGameRulesA');
+ __().unreal.gameRules.Remove(class'MockGameRulesB');
Issue(issueForNotAdded);
- TEST_ExpectFalse(__().unreal.AreGameRulesAdded(class'MockGameRulesA'));
- TEST_ExpectFalse(__().unreal.AreGameRulesAdded(class'MockGameRulesB'));
+ TEST_ExpectFalse(__().unreal.gameRules.AreAdded(class'MockGameRulesA'));
+ TEST_ExpectFalse(__().unreal.gameRules.AreAdded(class'MockGameRulesB'));
- __().unreal.AddGameRules(class'MockGameRulesB');
+ __().unreal.gameRules.Add(class'MockGameRulesB');
Issue(issueForNotAdded);
- TEST_ExpectFalse(__().unreal.AreGameRulesAdded(class'MockGameRulesA'));
+ TEST_ExpectFalse(__().unreal.gameRules.AreAdded(class'MockGameRulesA'));
Issue(issueForAdded);
- TEST_ExpectTrue(__().unreal.AreGameRulesAdded(class'MockGameRulesB'));
+ TEST_ExpectTrue(__().unreal.gameRules.AreAdded(class'MockGameRulesB'));
- __().unreal.AddGameRules(class'MockGameRulesA');
+ __().unreal.gameRules.Add(class'MockGameRulesA');
Issue(issueForAdded);
- TEST_ExpectTrue(__().unreal.AreGameRulesAdded(class'MockGameRulesA'));
- TEST_ExpectTrue(__().unreal.AreGameRulesAdded(class'MockGameRulesB'));
+ TEST_ExpectTrue(__().unreal.gameRules.AreAdded(class'MockGameRulesA'));
+ TEST_ExpectTrue(__().unreal.gameRules.AreAdded(class'MockGameRulesB'));
- __().unreal.RemoveGameRules(class'MockGameRulesB');
+ __().unreal.gameRules.Remove(class'MockGameRulesB');
Issue(issueForAdded);
- TEST_ExpectTrue(__().unreal.AreGameRulesAdded(class'MockGameRulesA'));
+ TEST_ExpectTrue(__().unreal.gameRules.AreAdded(class'MockGameRulesA'));
Issue(issueForNotAdded);
- TEST_ExpectFalse(__().unreal.AreGameRulesAdded(class'MockGameRulesB'));
+ TEST_ExpectFalse(__().unreal.gameRules.AreAdded(class'MockGameRulesB'));
}
protected static function Test_InventoryChainFetching()
@@ -137,7 +137,8 @@ protected static function Test_InventoryChainFetching()
chainEnd = chainEnd.inventory;
chainEnd.inventory = Inventory(__().memory.Allocate(class'MockInventoryA'));
chainEnd = chainEnd.inventory;
- chainEnd.inventory = Inventory(__().memory.Allocate(class'MockInventoryAChild'));
+ chainEnd.inventory =
+ Inventory(__().memory.Allocate(class'MockInventoryAChild'));
chainEnd = chainEnd.inventory;
chainEnd.inventory = Inventory(__().memory.Allocate(class'MockInventoryB'));
chainEnd = chainEnd.inventory;
diff --git a/sources/Unreal/UnrealAPI.uc b/sources/Unreal/UnrealAPI.uc
index c487216..38d2d79 100644
--- a/sources/Unreal/UnrealAPI.uc
+++ b/sources/Unreal/UnrealAPI.uc
@@ -20,6 +20,41 @@
*/
class UnrealAPI extends AcediaObject;
+var public GameRulesAPI gameRules;
+
+var private LoggerAPI.Definition errNoService;
+
+protected function Constructor()
+{
+ gameRules = GameRulesAPI(_.memory.Allocate(class'GameRulesAPI'));
+}
+
+/**
+ * Signal that will be emitted every tick.
+ *
+ * [Signature]
+ * void (float delta)
+ *
+ * @param delta In-game time in seconds that has passed since the last tick.
+ * To obtain real time passed from the last tick multiply `delta` by
+ * `1.1 / level.timeDilation`.
+ */
+/* SIGNAL */
+public final function Unreal_OnTick_Slot OnTick(
+ AcediaObject receiver)
+{
+ local Signal signal;
+ local UnrealService service;
+ service = UnrealService(class'UnrealService'.static.Require());
+ if (service == none)
+ {
+ _.logger.Auto(errNoService);
+ return none;
+ }
+ signal = service.GetSignal(class'Unreal_OnTick_Signal');
+ return Unreal_OnTick_Slot(signal.NewSlot(receiver));
+}
+
/**
* Returns current game's `LevelInfo`. Useful because `level` variable
* is not defined inside objects.
@@ -32,6 +67,33 @@ public final function LevelInfo GetLevel()
return class'CoreService'.static.GetInstance().level;
}
+/**
+ * Returns current game's `GameReplicationInfo`. Useful because `level.game`
+ * is not accessible inside objects.
+ *
+ * @return `GameReplicationInfo` instance for the current game. Guaranteed to
+ * not be `none`.
+ */
+public final function GameReplicationInfo GetGameRI()
+{
+ return class'CoreService'.static.GetInstance().level.GRI;
+}
+
+/**
+ * Returns current game's `GameReplicationInfo` as `KFGameReplicationInfo`.
+ * Useful because `level.game` is not accessible inside objects and because it
+ * auto converts game replication info type to `KFGameReplicationInfo`, which
+ * virtually all mods for killing floor use (by itself or as a base class).
+ *
+ * @return `KFGameReplicationInfo` instance for the current game.
+ * Can be `none` only if game was modded to run a `KFGameReplicationInfo`
+ * not derived from `KFGameType`.
+ */
+public final function KFGameReplicationInfo GetKFGameRI()
+{
+ return KFGameReplicationInfo(GetGameRI());
+}
+
/**
* Returns current game's `GameInfo`. Useful because `level.game` is not
* accessible inside objects.
@@ -71,89 +133,6 @@ public final function PlayerController GetLocalPlayer()
.GetLocalPlayerController();
}
-/**
- * Checks if given class of `GameRules` is currently active in `GameInfo`.
- *
- * @param rulesClassToCheck Class of rules to check for.
- * @return `true` if `GameRules` are active and `false` otherwise.
- */
-public final function bool AreGameRulesAdded(
- class rulesClassToCheck)
-{
- local GameRules rulesIter;
- if (rulesClassToCheck == none) {
- return false;
- }
- rulesIter = GetGameType().gameRulesModifiers;
- while (rulesIter != none)
- {
- if (rulesIter.class == rulesClassToCheck) {
- return true;
- }
- rulesIter = rulesIter.nextGameRules;
- }
- return false;
-}
-
-/**
- * Adds new `GameRules` class to the current `GameInfo`.
- * Does nothing if give `GameRules` class was already added before.
- *
- * @param newRulesClass Class of rules to add.
- * @return `true` if `GameRules` were added and `false` otherwise
- * (because they were already active.)
- */
-public final function bool AddGameRules(class newRulesClass)
-{
- if (AreGameRulesAdded(newRulesClass)) {
- return false;
- }
- GetGameType().AddGameModifier(GameRules(_.memory.Allocate(newRulesClass)));
- return true;
-}
-
-/**
- * Removes given `GameRules` class from the current `GameInfo`,
- * if they are active. Does nothing otherwise.
- *
- * @param rulesClassToRemove Class of rules to try and remove.
- * @return `true` if `GameRules` were removed and `false` otherwise
- * (if they were not active in the first place).
- */
-public final function bool RemoveGameRules(class rulesClassToRemove)
-{
- local GameInfo game;
- local GameRules rulesIter;
- local GameRules rulesToDestroy;
- if (rulesClassToRemove == none) return false;
- game = GetGameType();
- if (game.gameRulesModifiers == none) return false;
-
- // Check root rules
- rulesToDestroy = game.gameRulesModifiers;
- if (rulesToDestroy.class == rulesClassToRemove)
- {
- game.gameRulesModifiers = rulesToDestroy.nextGameRules;
- rulesToDestroy.Destroy();
- return true;
- }
- // Check rest of the rules
- rulesIter = game.gameRulesModifiers;
- while (rulesIter != none)
- {
- rulesToDestroy = rulesIter.nextGameRules;
- if ( rulesToDestroy != none
- && rulesToDestroy.class == rulesClassToRemove)
- {
- rulesIter.nextGameRules = rulesToDestroy.nextGameRules;
- rulesToDestroy.Destroy();
- return true;
- }
- rulesIter = rulesIter.nextGameRules;
- }
- return false;
-}
-
/**
* Convenience method for finding a first inventory entry of the given
* class `inventoryClass` in the given inventory chain `inventoryChain`.
@@ -257,4 +236,5 @@ public final function NativeActorRef ActorRef(optional Actor value)
defaultproperties
{
+ errNoService = (l=LOG_Error,m="`UnrealService` could not be reached.")
}
\ No newline at end of file
diff --git a/sources/Unreal/UnrealService.uc b/sources/Unreal/UnrealService.uc
new file mode 100644
index 0000000..f57bcb0
--- /dev/null
+++ b/sources/Unreal/UnrealService.uc
@@ -0,0 +1,92 @@
+/**
+ * Service for the needs of `UnrealAPI`. Mainly tasked with creating API's
+ * `Signal`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 UnrealService extends Service;
+
+struct SignalRecord
+{
+ var class signalClass;
+ var Signal instance;
+};
+var private array serviceSignals;
+var private Unreal_OnTick_Signal onTickSignal;
+
+protected function OnLaunch()
+{
+ local AcediaGameRules gameRules;
+ CreateSignals();
+ _.unreal.gameRules.Add(class'AcediaGameRules');
+ gameRules = AcediaGameRules(
+ _.unreal.gameRules.FindInstance(class'AcediaGameRules'));
+ gameRules.Initialize(self);
+}
+
+protected function OnShutdown()
+{
+ _.unreal.gameRules.Remove(class'AcediaGameRules');
+}
+
+private final function CreateSignals()
+{
+ local int i;
+ onTickSignal = Unreal_OnTick_Signal(
+ _.memory.Allocate(class'Unreal_OnTick_Signal'));
+ for (i = 0; i < serviceSignals.length; i += 1)
+ {
+ if (serviceSignals[i].instance != none) continue;
+ if (serviceSignals[i].signalClass == none) continue;
+
+ serviceSignals[i].instance =
+ Signal(_.memory.Allocate(serviceSignals[i].signalClass));
+ }
+}
+
+public final function Signal GetSignal(class requiredClass)
+{
+ local int i;
+ if (requiredClass == class'Unreal_OnTick_Signal') {
+ return onTickSignal;
+ }
+ for (i = 0; i < serviceSignals.length; i += 1)
+ {
+ if (serviceSignals[i].signalClass == requiredClass) {
+ return serviceSignals[i].instance;
+ }
+ }
+ return none;
+}
+
+
+public event Tick(float delta)
+{
+ local float dilationCoefficient;
+ if (onTickSignal != none)
+ {
+ dilationCoefficient = level.timeDilation / 1.1;
+ onTickSignal.Emit(delta, dilationCoefficient);
+ }
+}
+
+defaultproperties
+{
+ serviceSignals(0) = (signalClass=class'GameRules_OnFindPlayerStart_Signal')
+ serviceSignals(1) = (signalClass=class'GameRules_OnOverridePickupQuery_Signal')
+ serviceSignals(2) = (signalClass=class'GameRules_OnNetDamage_Signal')
+}
\ No newline at end of file