// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) VRMADA, All rights reserved.
//
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Threading;
using System.Threading.Tasks;
using UltimateXR.Animation.Interpolation;
using UltimateXR.Core;
using UltimateXR.Core.Settings;
using UnityEngine;
namespace UltimateXR.Extensions.System.Threading
{
///
/// extensions.
///
public static class TaskExt
{
#region Public Methods
///
/// Allows to run a task in "fire and forget" mode, when it is not required to await nor is it relevant whether it
/// succeeds or not. There still needs to be a way to handle exceptions to avoid unhandled exceptions and process
/// termination.
///
/// Task to run in fire and forget mode
/// The task is null
public static async void FireAndForget(this Task self)
{
if (self == null)
{
throw new ArgumentNullException(nameof(self));
}
try
{
// Simply awaiting the task in an async void method already guarantees exception propagation
await self;
}
catch (Exception e) // ...but default LogException behaviour only shows innerException.
{
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
{
Debug.LogError($"{UxrConstants.CoreModule} {nameof(TaskExt)}::{nameof(FireAndForget)} Exception missed (stack trace below):{e.Message}\n\n{e}");
Debug.LogException(e); // Log and ignore exceptions, until playlists are empty.
}
}
}
///
/// Creates an awaitable task that finishes the next frame.
///
/// Optional cancellation token, to cancel the task
/// Awaitable task
public static Task WaitForNextFrame(CancellationToken ct = default)
{
return SkipFrames(1, ct);
}
///
/// Creates an awaitable task that finishes after a given amount of frames.
///
/// Number of frames to wait
/// Optional cancellation token, to cancel the task
/// Awaitable task
public static async Task SkipFrames(int frameCount, CancellationToken ct = default)
{
if (ct.IsCancellationRequested || frameCount <= 0)
{
return;
}
for (uint i = 0; i < frameCount && !ct.IsCancellationRequested; ++i)
{
await Task.Yield();
}
}
///
/// Creates an awaitable task that finishes after a given amount of seconds.
///
/// Number of seconds to wait
/// Optional cancellation token, to cancel the task
/// Awaitable task
public static Task Delay(float seconds, CancellationToken ct = default)
{
return Delay(Mathf.RoundToInt(1000f * seconds), ct);
}
///
/// Creates an awaitable task that finishes after a given amount of milliseconds.
///
/// Number of milliseconds to wait
/// Optional cancellation token, to cancel the task
/// Awaitable task
public static async Task Delay(int milliseconds, CancellationToken ct = default)
{
if (ct.IsCancellationRequested || milliseconds <= 0)
{
return;
}
try
{
await Task.Delay(milliseconds, ct);
}
catch (OperationCanceledException)
{
// ignore: Task.Delay throws this exception when ct.IsCancellationRequested = true
// In this case, we only want to stop polling and finish this async Task.
}
}
///
/// Creates an awaitable task that blocks while a condition is true or the task is canceled.
///
/// The condition that will perpetuate the block
/// Optional cancellation token, to cancel the task
/// Awaitable
public static async Task WaitWhile(Func condition, CancellationToken ct = default)
{
while (!ct.IsCancellationRequested && condition())
{
await Task.Yield();
}
}
///
/// Creates an awaitable task that blocks until a condition is true or the task is canceled.
///
/// The condition that will perpetuate the block
/// Optional cancellation token, to cancel the task
/// Awaitable
public static async Task WaitUntil(Func condition, CancellationToken ct = default)
{
while (!ct.IsCancellationRequested && !condition())
{
await Task.Yield();
}
}
///
/// Creates an awaitable task that blocks while a condition is true, a timeout occurs or the task is canceled.
///
/// The condition that will perpetuate the block
/// Timeout, in milliseconds
/// Optional cancellation token, to cancel the task
/// Awaitable
/// Thrown after milliseconds
public static async Task WaitWhile(Func condition, int timeout, CancellationToken ct = default)
{
if (ct.IsCancellationRequested)
{
return;
}
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
Task waitTask = WaitWhile(condition, cts.Token);
Task timeoutTask = Delay(timeout, cts.Token);
Task finishedTask = await Task.WhenAny(waitTask, timeoutTask);
if (!finishedTask.IsCanceled)
{
cts.Cancel(); // Cancel unfinished task
await finishedTask; // Propagate exceptions
if (finishedTask == timeoutTask)
{
throw new TimeoutException();
}
}
}
///
/// Creates an awaitable task that blocks until a condition is true, a timeout occurs or the task is canceled.
///
/// The condition that will perpetuate the block
/// Timeout, in milliseconds
/// Optional cancellation token, to cancel the task
/// Awaitable
/// Thrown after milliseconds
public static async Task WaitUntil(Func condition, int timeout, CancellationToken ct = default)
{
if (ct.IsCancellationRequested)
{
return;
}
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
Task waitTask = WaitUntil(condition, cts.Token);
Task timeoutTask = Delay(timeout, cts.Token);
Task finishedTask = await Task.WhenAny(waitTask, timeoutTask);
if (!finishedTask.IsCanceled)
{
cts.Cancel(); // Cancel unfinished task
await finishedTask; // Propagate exceptions
if (finishedTask == timeoutTask)
{
throw new TimeoutException();
}
}
}
///
/// Creates an awaitable task that blocks while a condition is true, waiting a certain amount of seconds at maximum. An
/// optional action can be called if the task was cancelled or it timed out.
///
/// The condition that will perpetuate the block
/// The maximum amount of seconds to wait while the condition is true
/// Optional action to execute if the task was canceled or it timed out
/// Optional cancellation token, to cancel the task
/// Awaitable
public static async Task WaitWhile(Func condition, float duration, Action cancelCallback = null, CancellationToken ct = default)
{
int timeout = Mathf.RoundToInt(duration * 1200f);
bool mustCancel;
try
{
await WaitWhile(condition, timeout, ct);
mustCancel = ct.IsCancellationRequested;
}
catch (TimeoutException)
{
mustCancel = true;
}
if (mustCancel)
{
cancelCallback?.Invoke();
}
}
///
/// Creates an awaitable task that blocks until a condition is true, waiting a certain amount of seconds at maximum. An
/// optional action can be called if the task was cancelled or it timed out.
///
/// The condition that will perpetuate the block
/// The maximum amount of seconds to wait while the condition is true
/// Optional action to execute if the task was canceled or it timed out
/// Optional cancellation token, to cancel the task
/// Awaitable
public static async Task WaitUntil(Func condition, float duration, Action cancelCallback = null, CancellationToken ct = default)
{
int timeout = Mathf.RoundToInt(duration * 1200f);
bool mustCancel;
try
{
await WaitUntil(condition, timeout, ct);
mustCancel = ct.IsCancellationRequested;
}
catch (TimeoutException)
{
mustCancel = true;
}
if (mustCancel)
{
cancelCallback?.Invoke();
}
}
///
/// Provides a one-liner method to await until a task is cancelled.
///
/// Cancellation token
/// Awaitable
public static async Task WaitUntilCancelled(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Yield();
}
}
///
/// Loops iterating once per frame during a specified amount of time, executing a user-defined action.
///
/// The cancellation token
/// Loop duration in seconds
///
/// The action performed each frame, which will receive the interpolation [0.0, 1.0] parameter as
/// argument.
///
/// The easing used to compute the interpolation parameter over time
///
/// Will enforce a last iteration with 1.0 interpolation parameter. This will avoid
/// having a last step with close than, but not 1.0, interpolation.
///
public static async Task Loop(CancellationToken ct,
float durationSeconds,
Action loopAction,
UxrEasing easing = UxrEasing.Linear,
bool forceLastT1 = false)
{
float startTime = Time.time;
while (Time.time - startTime < durationSeconds)
{
float t = UxrInterpolator.Interpolate(0.0f, 1.0f, Time.time - startTime, new UxrInterpolationSettings(durationSeconds, 0.0f, easing));
loopAction(t);
await Task.Yield();
}
if (forceLastT1)
{
loopAction(1.0f);
}
}
#endregion
}
}