diff --git a/.gitignore b/.gitignore
index a800e90..c7f5f33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ obj/
.idea/
/ChaosBot/appsettings.json
/ChaosBot/ChaosBotSQL.db
+*.sln.DotSettings.user
diff --git a/ChaosBot.UnitTests/ChaosBot.UnitTests.csproj b/ChaosBot.UnitTests/ChaosBot.UnitTests.csproj
new file mode 100644
index 0000000..cc43bc8
--- /dev/null
+++ b/ChaosBot.UnitTests/ChaosBot.UnitTests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netcoreapp3.0
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ChaosBot.UnitTests/TimerTests.cs b/ChaosBot.UnitTests/TimerTests.cs
new file mode 100644
index 0000000..fc859f1
--- /dev/null
+++ b/ChaosBot.UnitTests/TimerTests.cs
@@ -0,0 +1,90 @@
+using System;
+using Antlr4.Runtime;
+using ChaosBot.Services;
+using NUnit.Framework;
+
+namespace ChaosBot.UnitTests
+{
+ public class TimerTests
+ {
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ public void RunAt_RunningTimerAtTimestamp_True()
+ {
+ DateTime current = DateTime.Now;
+ DateTime runAt = current + new TimeSpan(TimeSpan.TicksPerSecond * 10);
+ DateTime testAt = current + new TimeSpan(TimeSpan.TicksPerSecond * 2);
+
+ int totalRuns = 0;
+
+ void ToRun()
+ {
+ totalRuns++;
+ }
+
+ int timerId = Timer.RunAt(ToRun, runAt);
+
+ Assert.AreEqual(totalRuns, 0);
+ // Wait for the timer to finish
+ Timer.Join(timerId);
+ Assert.AreEqual(totalRuns, 1);
+ Assert.GreaterOrEqual(DateTime.Now, testAt);
+ }
+
+ [Test]
+ public void RunIn_RunningTimerInTimeSpan_True()
+ {
+ DateTime current = DateTime.Now;
+ TimeSpan runIn = new TimeSpan(TimeSpan.TicksPerSecond * 10);
+ DateTime testAt = current + new TimeSpan(TimeSpan.TicksPerSecond * 2);
+
+ int totalRuns = 0;
+
+ void ToRun()
+ {
+ totalRuns++;
+ }
+
+ int timerId = Timer.RunIn(ToRun, runIn);
+
+ Assert.AreEqual(totalRuns, 0);
+ // Wait for the timer to finish
+ Timer.Join(timerId);
+ Assert.AreEqual(totalRuns, 1);
+ Assert.GreaterOrEqual(DateTime.Now, testAt);
+ }
+
+ [Test]
+ public void RunTimer_RunningTimerCountingExecutions_True()
+ {
+ DateTime current = DateTime.Now;
+ TimeSpan interval = new TimeSpan(TimeSpan.TicksPerSecond * 2);
+ DateTime testAt = current + new TimeSpan(TimeSpan.TicksPerSecond * 11);
+
+ int totalRuns = 0;
+
+ void ToRun()
+ {
+ totalRuns++;
+ }
+
+ int timerId = Timer.RunTimer(ToRun, interval, offset: interval);
+
+ void CancelCallback()
+ {
+ Timer.CancelTimer(timerId);
+ }
+
+ int cancelTimerId = Timer.RunAt(CancelCallback, testAt);
+
+ Assert.AreEqual(totalRuns, 0);
+ // Wait for the cancel timer to finish
+ Timer.Join(cancelTimerId);
+ Assert.AreEqual(totalRuns, 5);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ChaosBot.sln b/ChaosBot.sln
index f35eeb4..712d969 100644
--- a/ChaosBot.sln
+++ b/ChaosBot.sln
@@ -2,6 +2,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChaosBot", "ChaosBot\ChaosBot.csproj", "{0222079F-84D7-4EEF-A2A6-D5AD67546D61}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChaosBot.UnitTests", "ChaosBot.UnitTests\ChaosBot.UnitTests.csproj", "{A4678BAA-93AF-4A10-8548-1921236BC57E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -12,5 +14,9 @@ Global
{0222079F-84D7-4EEF-A2A6-D5AD67546D61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0222079F-84D7-4EEF-A2A6-D5AD67546D61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0222079F-84D7-4EEF-A2A6-D5AD67546D61}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A4678BAA-93AF-4A10-8548-1921236BC57E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A4678BAA-93AF-4A10-8548-1921236BC57E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A4678BAA-93AF-4A10-8548-1921236BC57E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A4678BAA-93AF-4A10-8548-1921236BC57E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/ChaosBot/Services/Timer.cs b/ChaosBot/Services/Timer.cs
new file mode 100644
index 0000000..2300a17
--- /dev/null
+++ b/ChaosBot/Services/Timer.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ChaosBot.Services
+{
+ public static class Timer
+ {
+ private static Dictionary _timers = new Dictionary();
+
+ private static int _nextTimerIndex = 0;
+ private static int NextTimerIndex
+ {
+ get
+ {
+ _nextTimerIndex++;
+ return _nextTimerIndex;
+ }
+ }
+
+ public static int RunAt(Action toRun, DateTime deadlineTime)
+ {
+ DateTime current = DateTime.Now;
+ TimeSpan timeToGo = deadlineTime - current;
+ if (timeToGo < TimeSpan.Zero)
+ {
+ return -1; //time already passed
+ }
+
+ int timerIndex = NextTimerIndex;
+ _timers.Add(timerIndex, new System.Threading.Timer(x =>
+ {
+ toRun();
+ _timers.Remove(timerIndex);
+ }, null, timeToGo, Timeout.InfiniteTimeSpan));
+
+ return timerIndex;
+ }
+
+ public static int RunIn(Action toRun, TimeSpan dueTime)
+ {
+ DateTime current = DateTime.Now;
+ if (dueTime < TimeSpan.Zero)
+ {
+ return -1; //time already passed
+ }
+
+ int timerIndex = NextTimerIndex;
+ _timers.Add(timerIndex, new System.Threading.Timer(x =>
+ {
+ toRun();
+ _timers.Remove(timerIndex);
+ }, null, dueTime, Timeout.InfiniteTimeSpan));
+
+ return timerIndex;
+ }
+
+ public static int RunTimer(Action toRun, TimeSpan interval)
+ {
+ return RunTimer(toRun, interval, TimeSpan.Zero);
+ }
+
+ public static int RunTimer(Action toRun, TimeSpan interval, TimeSpan offset)
+ {
+ DateTime current = DateTime.Now;
+ if (offset < TimeSpan.Zero)
+ {
+ return -1; //time already passed
+ }
+
+ int timerIndex = NextTimerIndex;
+
+ void Callback()
+ {
+ toRun();
+
+ if (_timers.ContainsKey(timerIndex))
+ _timers.Remove(timerIndex);
+ _timers.Add(timerIndex, new System.Threading.Timer(x => { Callback(); }, null, interval, Timeout.InfiniteTimeSpan));
+ }
+
+ if (offset > TimeSpan.Zero)
+ {
+ _timers.Add(timerIndex, new System.Threading.Timer(x => { Callback(); }, null, offset, Timeout.InfiniteTimeSpan));
+ }
+ else
+ {
+ Callback();
+ }
+
+ return timerIndex;
+ }
+
+ public static void CancelTimer(int id)
+ {
+ if (id < 0) return;
+
+ if (!_timers.ContainsKey(id)) return;
+
+ _timers.GetValueOrDefault(id)?.Dispose();
+
+ if (_timers.ContainsKey(id))
+ _timers.Remove(id);
+ }
+
+ public static void Join(int id)
+ {
+ if (id < 0) return;
+
+ while (_timers.ContainsKey(id))
+ {
+ Thread.Sleep(500);
+ }
+ }
+ }
+}
\ No newline at end of file