diff --git a/sources/Global.uc b/sources/Global.uc
index c7b6767..3f9a88b 100644
--- a/sources/Global.uc
+++ b/sources/Global.uc
@@ -30,6 +30,7 @@ var public BoxAPI box;
var public LoggerAPI logger;
var public CollectionsAPI collections;
var public UnrealAPI unreal;
+var public TimeAPI time;
var public AliasesAPI alias;
var public TextAPI text;
var public MemoryAPI memory;
@@ -60,6 +61,7 @@ protected function Initialize()
text = TextAPI(memory.Allocate(class'TextAPI'));
collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI'));
unreal = UnrealAPI(memory.Allocate(class'UnrealAPI'));
+ time = TimeAPI(memory.Allocate(class'TimeAPI'));
logger = LoggerAPI(memory.Allocate(class'LoggerAPI'));
alias = AliasesAPI(memory.Allocate(class'AliasesAPI'));
console = ConsoleAPI(memory.Allocate(class'ConsoleAPI'));
diff --git a/sources/Time/Events/Timer_OnElapsed_Signal.uc b/sources/Time/Events/Timer_OnElapsed_Signal.uc
new file mode 100644
index 0000000..ec2cc82
--- /dev/null
+++ b/sources/Time/Events/Timer_OnElapsed_Signal.uc
@@ -0,0 +1,38 @@
+/**
+ * Signal class implementation for `Timer`'s `OnElapsed` 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 Timer_OnElapsed_Signal extends Signal;
+
+public final function Emit(Timer source)
+{
+ local Slot nextSlot;
+ StartIterating();
+ nextSlot = GetNextSlot();
+ while (nextSlot != none)
+ {
+ Timer_OnElapsed_Slot(nextSlot).connect(source);
+ nextSlot = GetNextSlot();
+ }
+ CleanEmptySlots();
+}
+
+defaultproperties
+{
+ relatedSlotClass = class'Timer_OnElapsed_Slot'
+}
\ No newline at end of file
diff --git a/sources/Time/Events/Timer_OnElapsed_Slot.uc b/sources/Time/Events/Timer_OnElapsed_Slot.uc
new file mode 100644
index 0000000..62dca7b
--- /dev/null
+++ b/sources/Time/Events/Timer_OnElapsed_Slot.uc
@@ -0,0 +1,40 @@
+/**
+ * Slot class implementation for `Timer`'s `OnElapsed` 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 Timer_OnElapsed_Slot extends Slot;
+
+delegate connect(Timer source)
+{
+ DummyCall();
+}
+
+protected function Constructor()
+{
+ connect = none;
+}
+
+protected function Finalizer()
+{
+ super.Finalizer();
+ connect = none;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Time/RealTimer.uc b/sources/Time/RealTimer.uc
new file mode 100644
index 0000000..0b05c7a
--- /dev/null
+++ b/sources/Time/RealTimer.uc
@@ -0,0 +1,32 @@
+/**
+ * Class that acts like `Timer`, except it measures real, instead of in-game,
+ * time.
+ * 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 RealTimer extends Timer;
+
+protected function float HandleTimeDilation(
+ float timeDelta,
+ float dilationCoefficient)
+{
+ return timeDelta / dilationCoefficient;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Time/TimeAPI.uc b/sources/Time/TimeAPI.uc
new file mode 100644
index 0000000..9bce64d
--- /dev/null
+++ b/sources/Time/TimeAPI.uc
@@ -0,0 +1,108 @@
+/**
+ * API that provides time-related methods.
+ * 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 TimeAPI extends AcediaObject;
+
+/**
+ * Creates new `Timer`. Does not start it.
+ *
+ * @param interval Returned `Timer` will be configured to emit
+ * `OnElapsed()` signals every `interval` seconds.
+ * @param autoReset `true` will configure caller `Timer` to repeatedly emit
+ * `OnElapsed()` every `interval` seconds, `false` (default value) will
+ * make returned `Timer` emit that signal only once.
+ * @return `Timer`, configured to emit `OnElapsed()` every `interval` seconds.
+ * Not started. Guaranteed to be not `none`.
+ */
+public final function Timer NewTimer(
+ optional float interval,
+ optional bool autoReset)
+{
+ return Timer(_.memory.Allocate(class'Timer'))
+ .SetInterval(interval)
+ .SetAutoReset(autoReset);
+}
+
+/**
+ * Creates and starts new `Timer`.
+ *
+ * @param interval Returned `Timer` will be configured to emit
+ * `OnElapsed()` signals every `interval` seconds.
+ * @param autoReset Setting this to `true` will configure caller `Timer` to
+ * repeatedly emit `OnElapsed()` signal every `interval` seconds, `false`
+ * (default value) will make returned `Timer` emit that signal only once.
+ * @return `Timer`, configured to emit `OnElapsed()` every `interval` seconds.
+ * Guaranteed to be not `none`.
+ */
+public final function Timer StartTimer(float interval, optional bool autoReset)
+{
+ return Timer(_.memory.Allocate(class'Timer'))
+ .SetInterval(interval)
+ .SetAutoReset(autoReset)
+ .Start();
+}
+
+/**
+ * Creates new `RealTimer`. Does not start it.
+ *
+ * @param interval Returned `RealTimer` will be configured to emit
+ * `OnElapsed()` signals every `interval` seconds.
+ * @param autoReset `true` will configure caller `RealTimer` to repeatedly
+ * emit `OnElapsed()` every `interval` seconds, `false` (default value)
+ * will make returned `RealTimer` emit that signal only once.
+ * @return `RealTimer`, configured to emit `OnElapsed()` every `interval`
+ * seconds. Not started. Guaranteed to be not `none`.
+ */
+public final function RealTimer NewRealTimer(
+ optional float interval,
+ optional bool autoReset)
+{
+ local RealTimer newTimer;
+ newTimer = RealTimer(_.memory.Allocate(class'RealTimer'));
+ newTimer.SetInterval(interval).SetAutoReset(autoReset);
+ return newTimer;
+}
+
+/**
+ * Creates and starts new `RealTimer`.
+ *
+ * @param interval Returned `RealTimer` will be configured to emit
+ * `OnElapsed()` signals every `interval` seconds.
+ * @param autoReset Setting this to `true` will configure caller `RealTimer`
+ * to repeatedly emit `OnElapsed()` signal every `interval` seconds,
+ * `false` (default value) will make returned `RealTimer` emit that signal
+ * only once.
+ * @return `RealTimer`, configured to emit `OnElapsed()` every `interval`
+ * seconds. Guaranteed to be not `none`.
+ */
+public final function RealTimer StartRealTimer(
+ float interval,
+ optional bool autoReset)
+{
+ local RealTimer newTimer;
+ newTimer = RealTimer(_.memory.Allocate(class'RealTimer'));
+ newTimer.SetInterval(interval)
+ .SetAutoReset(autoReset)
+ .Start();
+ return newTimer;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Time/Timer.uc b/sources/Time/Timer.uc
new file mode 100644
index 0000000..844b3e6
--- /dev/null
+++ b/sources/Time/Timer.uc
@@ -0,0 +1,248 @@
+/**
+ * Timer class that generates a signal after a set interval, with an option
+ * to generate recurring signals.
+ * 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 Timer extends AcediaObject;
+
+/**
+ * Because the `Timer` depends on the `Tick()` event, it has the same
+ * resolution as a server's tick rate (frame rate for clients). This means that
+ * the `OnElapsed()` signal will be emitted at an interval defined by the
+ * `Tick()`. If the set interval between two signals is less than that, then
+ * `Timer` might emit several signals at the same point of time.
+ * Supposing server's tick rate is 30, but `Timer` is set to emit a signal
+ * 60 time per second, then on average it will emit two signals each tick at
+ * the same exact time.
+ */
+
+// Is timer currently tracking time until the next event?
+var private bool isTimerEnabled;
+// Should timer automatically reset after the next event to
+// also generate recurring signals?
+var private bool isTimerAutoReset;
+// Currently elapsed time since this timer has started waiting for the
+// next event
+var private float totalElapsedTime;
+// Time interval between timer's start and generating the signal
+var private float eventInterval;
+// This flag tells `Timer` to stop trying to emit messages that accumulated
+// between two `Tick()` updates. Used in case `Timer` was disables or
+// stopped during one of them.
+var private bool clearEventQueue;
+
+var private Timer_OnElapsed_Signal onElapsedSignal;
+
+protected function Constructor()
+{
+ onElapsedSignal = Timer_OnElapsed_Signal(
+ _.memory.Allocate(class'Timer_OnElapsed_Signal'));
+}
+
+protected function Finalizer()
+{
+ _.memory.Free(onElapsedSignal);
+ StopMe(); // Disconnects from listening to `_.unreal.OnTick()`
+}
+
+/**
+ * Signal that will be emitted every time timer's interval is elapsed.
+ *
+ * [Signature]
+ * void (Timer source)
+ *
+ * @param source `Timer` that emitted the signal.
+ */
+/* SIGNAL */
+public final function Timer_OnElapsed_Slot OnElapsed(AcediaObject receiver)
+{
+ return Timer_OnElapsed_Slot(onElapsedSignal.NewSlot(receiver));
+}
+
+/**
+ * This method is called every tick while the caller `Timer` is running and
+ * can be overloaded to modify how passed time is affected by the
+ * time dilation.
+ *
+ * For example, to make caller `Timer` count real time you need to passed time
+ * by the `dilationCoefficient`:
+ * `return timeDelta / dilationCoefficient;`.
+ *
+ * @param timeDelta In-game time that has passed since the last
+ * `Tick()` event. To obtain real time should be divided by
+ * `dilationCoefficient`.
+ * @param dilationCoefficient Coefficient of time dilation for the passed
+ * `Tick()`. Regular speed is `1.0` (corrected from native value `1.1`
+ * for Unreal Engine 2).
+ */
+protected function float HandleTimeDilation(
+ float timeDelta,
+ float dilationCoefficient)
+{
+ return timeDelta;
+}
+
+/**
+ * Returns current interval between `OnElapsed()` signals for the
+ * caller `Timer`. In seconds.
+ *
+ * @return How many seconds separate two `OnElapsed()` signals
+ * (or starting a timer and next `OnElapsed()` event).
+ */
+public final function float GetInterval()
+{
+ return eventInterval;
+}
+
+/**
+ * Sets current interval between `OnElapsed()` signals for the
+ * caller `Timer`. In seconds.
+ *
+ * Setting this value while the caller `Timer` is running resets it (same as
+ * calling `StopMe().Start()`).
+ *
+ * @param newInterval How many seconds should separate two `OnElapsed()`
+ * signals (or starting a timer and next `OnElapsed()` event)?
+ * Setting a value `<= 0` disables the timer.
+ * @return Caller `Timer` to allow for method chaining.
+ */
+public final function Timer SetInterval(float newInterval)
+{
+ eventInterval = newInterval;
+ if (eventInterval <= 0)
+ {
+ StopMe();
+ return self;
+ }
+ if (isTimerEnabled) {
+ Start();
+ }
+ return self;
+}
+
+/**
+ * Checks whether the timer is currently enabled (emitting signals with
+ * set interval).
+ *
+ * @return `true` if caller `Timer` is enabled and `false` otherwise.
+ */
+public final function bool IsEnabled()
+{
+ return isTimerEnabled;
+}
+
+/**
+ * Checks whether this `Timer` would automatically reset after the emitted
+ * `OnElapsed()` signal, allowing for recurring signals.
+ *
+ * @return `true` if `Timer` will emit `OnElapse()` signal each time
+ * the interval elapses and `false` otherwise.
+ */
+public final function bool IsAutoReset(float newInterval)
+{
+ return isTimerAutoReset;
+}
+
+/**
+ * Sets whether this `Timer` would automatically reset after the emitted
+ * `OnElapsed()` signal, allowing for recurring signals.
+ *
+ * @param doAutoReset `true` if `Timer` will emit `OnElapse()` signal
+ * each time the interval elapses and `false` otherwise.
+ * @return Caller `Timer` to allow for method chaining.
+ */
+public final function Timer SetAutoReset(bool doAutoReset)
+{
+ isTimerAutoReset = doAutoReset;
+ return self;
+}
+
+/**
+ * Starts emitting `OneElapsed()` signal.
+ *
+ * Does nothing if current timer interval (set by `SetInterval()`) is set
+ * to a value that's `<= 0`.
+ *
+ * If caller `Timer` is already running, resets it (same as calling
+ * `StopMe().Start()`).
+ *
+ * @return Caller `Timer` to allow for method chaining.
+ */
+public final function Timer Start()
+{
+ if (eventInterval <= 0) {
+ return self;
+ }
+ if (!isTimerEnabled) {
+ _.unreal.OnTick(self).connect = Tick;
+ }
+ isTimerEnabled = true;
+ totalElapsedTime = 0.0;
+ return self;
+}
+
+/**
+ * Stops emitting `OneElapsed()` signal.
+ *
+ * @return Caller `Timer` to allow for method chaining.
+ */
+public final function Timer StopMe()
+{
+ _.unreal.OnTick(self).Disconnect();
+ isTimerEnabled = false;
+ clearEventQueue = true;
+ return self;
+}
+
+/**
+ * Returns currently elapsed time since caller `Timer` has started waiting for
+ * the next event.
+ *
+ * @return Elapsed time since caller `Timer` has started.
+ */
+public final function float GetElapsedTime()
+{
+ return totalElapsedTime;
+}
+
+private final function Tick(float delta, float dilationCoefficient)
+{
+ if (onElapsedSignal == none || eventInterval <= 0.0)
+ {
+ StopMe();
+ return;
+ }
+ totalElapsedTime += HandleTimeDilation(delta, dilationCoefficient);
+ clearEventQueue = false;
+ while (totalElapsedTime > eventInterval && !clearEventQueue)
+ {
+ // It is important to modify _before_ the signal call in case `Timer`
+ // is reset there and already has a zeroed `totalElapsedTime`
+ totalElapsedTime -= eventInterval;
+ onElapsedSignal.Emit(self);
+ if (!isTimerAutoReset)
+ {
+ StopMe();
+ return;
+ }
+ }
+}
+
+defaultproperties
+{
+}
\ No newline at end of file