Code/Timer.cs
// sbox.Community © 2023-2024
// https://github.com/sbox-community/sbox-timer-lib/blob/main/Timer.cs#L276
using Sandbox;
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading;
public partial class Timer : IDisposable
{
private static Dictionary<string, Timer> activeTimers = new Dictionary<string, Timer>();
public CancellationTokenSource? CTS;
private TaskCompletionSource<bool>? TCS;
public Process status;
private float delay;
private string id;
private int repetitions;
private Action func;
private float nextExecution;
private bool isDisposed = false;
private bool duplicated = false;
public enum Process
{
Continue,
Pause,
UnPause,
Stop,
Start,
}
public Timer( float delay, string id, int repetitions, Action func, bool threaded = false )
{
Remove( id );
CTS = new();
TCS = new();
status = Process.Continue;
this.delay = delay;
this.id = id;
this.repetitions = repetitions;
this.func = func;
if ( threaded )
GameTask.RunInThreadAsync( Loop );
else
Loop();
}
~Timer()
{
Dispose( true );
}
private async void Loop()
{
lock ( activeTimers )
{
if ( Exists( id ) )
activeTimers[id].duplicated = true;
activeTimers[id] = this;
}
var _repetitions = repetitions;
var pausedDelay = 0f;
while ( true )
{
if ( isDisposed || duplicated )
return;
nextExecution = Time.Now;
try
{
if ( status == Process.Pause || status == Process.Stop )
await TCS.Task.WaitAsync( CTS.Token );
else
await Task.Delay( TimeSpan.FromSeconds( pausedDelay != 0f ? pausedDelay : delay ), CTS.Token );
}
catch ( TaskCanceledException )
{
switch ( status )
{
case (Process.Continue):
Dispose();
return;
case (Process.Pause):
newCTS();
pausedDelay = MathF.Max( delay - (Time.Now - nextExecution), 0f );
continue;
case (Process.UnPause):
newCTS();
status = Process.Continue;
continue;
case (Process.Stop):
newCTS();
repetitions = _repetitions;
pausedDelay = 0f;
continue;
case (Process.Start):
newCTS();
repetitions = _repetitions;
pausedDelay = 0f;
status = Process.Continue;
continue;
default:
Dispose();
return;
}
}
if ( pausedDelay != 0f )
pausedDelay = 0f;
if ( !Game.InGame )
{
Dispose();
return;
}
try
{
func?.Invoke();
}
catch { }
if ( repetitions != -1 && repetitions-- == 0 )
{
Dispose();
return;
}
}
}
public void Dispose()
{
if ( !isDisposed && !duplicated )
activeTimers.Remove( id );
Dispose( true );
GC.SuppressFinalize( this );
}
private void Dispose( bool isdisposing )
{
if ( !isDisposed )
{
if ( isdisposing )
{
CTS?.Cancel();
try
{
CTS?.Dispose();
}
catch { }
CTS = null;
_ = TCS?.TrySetCanceled();
TCS = null;
id = null;
func = null;
}
isDisposed = true;
}
}
private void newCTS()
{
CTS?.Dispose();
CTS = new();
}
public static bool Remove( string id )
{
lock ( activeTimers )
{
if ( Exists( id ) )
{
activeTimers[id].Dispose();
return true;
}
return false;
}
}
public static void Simple( float delay, Action func, bool threaded = false )
{
if ( threaded )
GameTask.RunInThreadAsync( async () => await Task.Delay( TimeSpan.FromSeconds( delay ) ).ContinueWith(_ => func?.Invoke()) ); else
Simple( delay, func );
}
async private static void Simple( float delay, Action func )
{
await Task.Delay( TimeSpan.FromSeconds( delay ) );
try
{
func?.Invoke();
}
catch { }
}
public static void Create( string id, float delay, int repetitions, Action func, bool threaded = false ) => new Timer( delay, id, repetitions == 0 ? -1 : repetitions - 1, func, threaded );
public static bool Exists( string id )
{
lock ( activeTimers )
{
return activeTimers.ContainsKey( id );
}
}
public static float TimeLeft( string id )
{
lock ( activeTimers )
{
return Exists( id ) ? (activeTimers[id].nextExecution + activeTimers[id].delay) - Time.Now : 0f;
}
}
public static bool Adjust( string id, float? delay = null, int? repetitions = null, Action? func = null )
{
lock ( activeTimers )
{
if ( Exists( id ) )
{
if ( delay is not null )
activeTimers[id].delay = delay.Value;
if ( repetitions is not null )
activeTimers[id].repetitions = repetitions.Value == 0 ? -1 : repetitions.Value;
if ( func is not null )
activeTimers[id].func = func;
return true;
}
return false;
}
}
public static int RepsLeft( string id )
{
lock ( activeTimers )
{
return Exists( id ) ? activeTimers[id].repetitions : -1;
}
}
public static bool Toggle( string id )
{
lock ( activeTimers )
{
if ( Exists( id ) )
{
var result = activeTimers[id].status;
if ( result == Process.Continue || result == Process.Pause )
activeTimers[id].status = activeTimers[id].status == Process.Continue ? Process.Pause : Process.UnPause;
activeTimers[id].CTS.Cancel();
return activeTimers[id].status == Process.Pause;
}
return false;
}
}
public static bool Pause( string id ) => UpdateStatus( id, Process.Pause );
public static bool UnPause( string id ) => UpdateStatus( id, Process.UnPause );
public static bool Start( string id ) => UpdateStatus( id, Process.Start );
public static bool Stop( string id ) => UpdateStatus( id, Process.Stop );
private static bool UpdateStatus( string id, Process status )
{
lock ( activeTimers )
{
if ( Exists( id ) )
{
activeTimers[id].status = status;
activeTimers[id].CTS.Cancel();
return true;
}
return false;
}
}
public static Dictionary<string, Timer> All() => activeTimers;
public static void PrintAll()
{
foreach ( var timer in activeTimers )
Log.Info( $"{timer.Key} => delay: {timer.Value.delay}, repetitions: {timer.Value.repetitions}, status: {timer.Value.status}" );
}
}