diff --git a/src/SampSharp.OpenMp.Entities/Timers/StopwatchTime.cs b/src/SampSharp.OpenMp.Entities/Timers/StopwatchTime.cs
new file mode 100644
index 00000000..575180c9
--- /dev/null
+++ b/src/SampSharp.OpenMp.Entities/Timers/StopwatchTime.cs
@@ -0,0 +1,29 @@
+using System.Diagnostics;
+
+namespace SampSharp.Entities;
+
+internal static class StopwatchTime
+{
+ ///
+ /// Converts a TimeSpan to Stopwatch ticks.
+ ///
+ /// IMPORTANT:
+ /// - TimeSpan ticks are fixed (1 tick = 100ns, 10,000,000 per second)
+ /// - Stopwatch ticks depend on hardware (Stopwatch.Frequency)
+ ///
+ /// This method converts a TimeSpan duration to the equivalent number
+ /// of Stopwatch ticks so both values can be used in the same time system.
+ ///
+ public static long ToStopwatchTicks(TimeSpan time)
+ => (long)(time.TotalSeconds * Stopwatch.Frequency);
+
+ ///
+ /// Converts Stopwatch ticks to a TimeSpan.
+ ///
+ /// This is the inverse operation of ToStopwatchTicks.
+ /// It converts a duration measured in Stopwatch ticks back into
+ /// a TimeSpan using Stopwatch.Frequency.
+ ///
+ public static TimeSpan ToTimeSpan(long stopwatchTicks)
+ => TimeSpan.FromSeconds(stopwatchTicks / (double)Stopwatch.Frequency);
+}
diff --git a/src/SampSharp.OpenMp.Entities/Timers/TimerReference.cs b/src/SampSharp.OpenMp.Entities/Timers/TimerReference.cs
index 20c58456..8d242fee 100644
--- a/src/SampSharp.OpenMp.Entities/Timers/TimerReference.cs
+++ b/src/SampSharp.OpenMp.Entities/Timers/TimerReference.cs
@@ -18,7 +18,14 @@ internal TimerReference(TimerInfo info, object? target, MethodInfo? method)
///
/// Gets the time span until the next tick of this timer.
///
- public TimeSpan NextTick => new(Info.NextTick - Stopwatch.GetTimestamp());
+ public TimeSpan NextTick
+ {
+ get
+ {
+ long remainingStopwatchTicks = Info.NextTick - Stopwatch.GetTimestamp();
+ return StopwatchTime.ToTimeSpan(remainingStopwatchTicks);
+ }
+ }
internal TimerInfo Info { get; set; }
diff --git a/src/SampSharp.OpenMp.Entities/Timers/TimerSystem.cs b/src/SampSharp.OpenMp.Entities/Timers/TimerSystem.cs
index ab6aa199..d8336b80 100644
--- a/src/SampSharp.OpenMp.Entities/Timers/TimerSystem.cs
+++ b/src/SampSharp.OpenMp.Entities/Timers/TimerSystem.cs
@@ -42,6 +42,8 @@ public void Tick()
{
var timer = _timers[i];
+ // IMPORTANT:
+ // Both NextTick and timestamp are in Stopwatch ticks, so the comparison is valid across platforms.
while (timer.NextTick <= timestamp)
{
try
@@ -97,7 +99,15 @@ public TimerReference Start(Action action, Tim
throw new ArgumentOutOfRangeException(nameof(interval), interval, "The interval should be a nonzero positive value.");
}
- var invoker = new TimerInfo(intervalTicks: interval.Ticks, nextTick: Stopwatch.GetTimestamp() + interval.Ticks, invoke: null!, true);
+ long intervalStopwatchTicks = StopwatchTime.ToStopwatchTicks(interval);
+ var invoker = new TimerInfo(
+ intervalTicks: intervalStopwatchTicks,
+ // IMPORTANT:
+ // Keeping everything in Stopwatch ticks ensures correct behavior cross-plataform.
+ nextTick: Stopwatch.GetTimestamp() + intervalStopwatchTicks,
+ invoke: null!,
+ isActive: true
+ );
var reference = new TimerReference(invoker, action.Target, action.Method);
@@ -157,11 +167,15 @@ private void CreateTimersFromAssemblies()
LogLowInterval(target, method.Name, attribute.IntervalTimeSpan);
}
+ long intervalStopwatchTicks = StopwatchTime.ToStopwatchTicks(attribute.IntervalTimeSpan);
var timer = new TimerInfo(
- intervalTicks: attribute.IntervalTimeSpan.Ticks,
- nextTick: tick + attribute.IntervalTimeSpan.Ticks,
+ intervalTicks: intervalStopwatchTicks,
+ // IMPORTANT:
+ // Keeping everything in Stopwatch ticks ensures correct behavior cross-plataform.
+ nextTick: tick + intervalStopwatchTicks,
invoke: () => compiled(service, null, _serviceProvider, null),
- isActive: true);
+ isActive: true
+ );
timer.Reference = new TimerReference(timer, service, method);