diff --git a/IPA.Loader/Utilities/AlmostVersion.cs b/IPA.Loader/Utilities/AlmostVersion.cs
index 6b9c8111..a9527503 100644
--- a/IPA.Loader/Utilities/AlmostVersion.cs
+++ b/IPA.Loader/Utilities/AlmostVersion.cs
@@ -190,7 +190,7 @@ namespace IPA.Utilities
/// the object to compare to
/// if they are equal, otherwise
///
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
return obj is AlmostVersion version &&
SemverValue == version.SemverValue &&
diff --git a/IPA.Loader/Utilities/Async/Coroutines.cs b/IPA.Loader/Utilities/Async/Coroutines.cs
index b474e566..2062da28 100644
--- a/IPA.Loader/Utilities/Async/Coroutines.cs
+++ b/IPA.Loader/Utilities/Async/Coroutines.cs
@@ -54,10 +54,10 @@ namespace IPA.Utilities.Async
public static Task AsTask(IEnumerator coroutine)
{
if (!UnityGame.OnMainThread)
- return UnityMainThreadTaskScheduler.Factory.StartNew(() => AsTask(coroutine)).Unwrap();
+ return UnityMainThreadTaskScheduler.Factory.StartNew(() => AsTask(coroutine), default, default, UnityMainThreadTaskScheduler.Default).Unwrap();
var tcs = new TaskCompletionSource(coroutine, AsTaskSourceOptions);
- PluginComponent.Instance.StartCoroutine(new AsTaskCoroutineExecutor(coroutine, tcs));
+ _ = PluginComponent.Instance.StartCoroutine(new AsTaskCoroutineExecutor(coroutine, tcs));
return tcs.Task;
}
@@ -85,7 +85,7 @@ namespace IPA.Utilities.Async
enumerators.Push(coroutine);
}
- private readonly Stack enumerators = new Stack(2);
+ private readonly Stack enumerators = new(2);
public object Current => enumerators.FirstOrDefault()?.Current; // effectively a TryPeek
@@ -116,7 +116,7 @@ namespace IPA.Utilities.Async
}
else
{ // this enumerator completed, so pop it and continue
- enumerators.Pop();
+ _ = enumerators.Pop();
continue;
}
}
diff --git a/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs b/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs
index e3fb3f54..fa4aea3a 100644
--- a/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs
+++ b/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs
@@ -1,12 +1,11 @@
-using System;
+#nullable enable
+using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
-using System.Text;
-using System.Threading;
using System.Threading.Tasks;
namespace IPA.Utilities.Async
@@ -20,15 +19,15 @@ namespace IPA.Utilities.Async
/// Gets the default main thread scheduler that is managed by BSIPA.
///
/// a scheduler that is managed by BSIPA
- public static new TaskScheduler Default { get; } = new UnityMainThreadTaskScheduler();
+ public static new UnityMainThreadTaskScheduler Default { get; } = new UnityMainThreadTaskScheduler();
///
/// Gets a factory for creating tasks on .
///
/// a factory for creating tasks on the default scheduler
public static TaskFactory Factory { get; } = new TaskFactory(Default);
- private readonly ConcurrentQueue tasks = new ConcurrentQueue();
- private static readonly ConditionalWeakTable itemTable = new ConditionalWeakTable();
+ private readonly ConcurrentQueue tasks = new();
+ private static readonly ConditionalWeakTable itemTable = new();
private class QueueItem : IEquatable, IEquatable
{
@@ -43,7 +42,9 @@ namespace IPA.Utilities.Async
}
}
- public Task Task { get; private set; } = null;
+ public Task? Task { get; private set; }
+
+ public Action? Action { get; private set; }
public QueueItem(Task task)
{
@@ -51,7 +52,13 @@ namespace IPA.Utilities.Async
Task = task;
}
- public bool Equals(Task other) => HasTask && other.Equals(Task);
+ public QueueItem(Action action)
+ {
+ HasTask = true;
+ Action = action;
+ }
+
+ public bool Equals(Task? other) => other is not null && HasTask && other.Equals(Task);
public bool Equals(QueueItem other) => other.HasTask == HasTask && Equals(other.Task);
}
@@ -146,7 +153,11 @@ namespace IPA.Utilities.Async
do if (!tasks.TryDequeue(out task)) goto exit; // try dequeue, if we can't exit
while (!task.HasTask); // if the dequeued task is empty, try again
- TryExecuteTask(task.Task);
+ if (task.Task is not null)
+ {
+ _ = TryExecuteTask(task.Task);
+ }
+ task.Action?.Invoke();
}
exit:
sw.Reset();
@@ -181,7 +192,7 @@ namespace IPA.Utilities.Async
/// nothing
/// Always.
protected override IEnumerable GetScheduledTasks()
- => tasks.ToArray().Where(q => q.HasTask).Select(q => q.Task).ToArray();
+ => tasks.ToArray().Where(q => q.HasTask).Select(q => q.Task).NonNull().ToArray();
///
/// Queues a given to this scheduler. The must be
@@ -198,6 +209,13 @@ namespace IPA.Utilities.Async
tasks.Enqueue(item);
}
+ internal void QueueAction(Action action)
+ {
+ ThrowIfDisposed();
+
+ tasks.Enqueue(new(action));
+ }
+
///
/// Runs the task inline if the current thread is the Unity main thread.
///
diff --git a/IPA.Loader/Utilities/UnityGame.cs b/IPA.Loader/Utilities/UnityGame.cs
index 9765af75..e87c414d 100644
--- a/IPA.Loader/Utilities/UnityGame.cs
+++ b/IPA.Loader/Utilities/UnityGame.cs
@@ -1,6 +1,9 @@
-using IPA.Config;
+#nullable enable
+using IPA.Config;
+using IPA.Utilities.Async;
using System;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
@@ -17,12 +20,12 @@ namespace IPA.Utilities
///
public static class UnityGame
{
- private static AlmostVersion _gameVersion;
+ private static AlmostVersion? _gameVersion;
///
/// Provides the current game version.
///
/// the SemVer version of the game
- public static AlmostVersion GameVersion => _gameVersion ?? (_gameVersion = new AlmostVersion(ApplicationVersionProxy));
+ public static AlmostVersion GameVersion => _gameVersion ??= new AlmostVersion(ApplicationVersionProxy);
internal static void SetEarlyGameVersion(AlmostVersion ver)
{
@@ -76,24 +79,30 @@ namespace IPA.Utilities
}
internal static bool IsGameVersionBoundary { get; private set; }
- internal static AlmostVersion OldVersion { get; private set; }
+ internal static AlmostVersion? OldVersion { get; private set; }
internal static void CheckGameVersionBoundary()
{
var gameVer = GameVersion;
var lastVerS = SelfConfig.LastGameVersion_;
OldVersion = lastVerS != null ? new AlmostVersion(lastVerS, gameVer) : null;
- IsGameVersionBoundary = OldVersion != null && gameVer != OldVersion;
+ IsGameVersionBoundary = OldVersion is not null && gameVer != OldVersion;
SelfConfig.Instance.LastGameVersion = gameVer.ToString();
}
- private static Thread mainThread;
+ private static Thread? mainThread;
///
/// Checks if the currently running code is running on the Unity main thread.
///
/// if the curent thread is the Unity main thread, otherwise
- public static bool OnMainThread => Thread.CurrentThread.ManagedThreadId == mainThread?.ManagedThreadId;
+ public static bool OnMainThread => Environment.CurrentManagedThreadId == mainThread?.ManagedThreadId;
+
+ ///
+ /// Asynchronously switches the current execution context to the Unity main thread.
+ ///
+ /// An awaitable which causes any following code to execute on the main thread.
+ public static SwitchToUnityMainThreadAwaitable SwitchToMainThreadAsync() => default;
internal static void SetMainThread()
=> mainThread = Thread.CurrentThread;
@@ -120,9 +129,9 @@ namespace IPA.Utilities
/// This only gives a
///
/// the type of release this is
- public static Release ReleaseType => (_releaseCache ?? (_releaseCache = CheckIsSteam() ? Release.Steam : Release.Other)).Value;
+ public static Release ReleaseType => _releaseCache ??= CheckIsSteam() ? Release.Steam : Release.Other;
- private static string _installRoot;
+ private static string? _installRoot;
///
/// Gets the path to the game's install directory.
///
@@ -165,4 +174,59 @@ namespace IPA.Utilities
&& installDirInfo.Parent?.Parent?.Name == "steamapps";
}
}
+
+ ///
+ /// An awaitable which, when awaited, switches the current context to the Unity main thread.
+ ///
+ ///
+ [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types",
+ Justification = "This type should never be compared.")]
+ public struct SwitchToUnityMainThreadAwaitable
+ {
+ ///
+ /// Gets the awaiter for this awaitable.
+ ///
+ /// The awaiter for this awaitable.
+ public SwitchToUnityMainThreadAwaiter GetAwaiter() => default;
+ }
+
+ ///
+ /// An awaiter which, when awaited, switches the current context to the Unity main thread.
+ ///
+ ///
+ [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types",
+ Justification = "This type should never be compared.")]
+ public struct SwitchToUnityMainThreadAwaiter : INotifyCompletion, ICriticalNotifyCompletion
+ {
+ private static readonly ContextCallback InvokeAction = static o => ((Action)o!)();
+
+ ///
+ /// Gets whether or not this awaiter is completed.
+ ///
+ public bool IsCompleted => UnityGame.OnMainThread;
+
+ ///
+ /// Gets the result of this awaiter.
+ ///
+ public void GetResult() { }
+
+ ///
+ /// Registers a continuation to be called when this awaiter finishes.
+ ///
+ /// The continuation.
+ public void OnCompleted(Action continuation)
+ {
+ var ec = ExecutionContext.Capture();
+ UnityMainThreadTaskScheduler.Default.QueueAction(() => ExecutionContext.Run(ec, InvokeAction, continuation));
+ }
+
+ ///
+ /// Registers a continuation to be called when this awaiter finishes, without capturing the execution context.
+ ///
+ /// The continuation.
+ public void UnsafeOnCompleted(Action continuation)
+ {
+ UnityMainThreadTaskScheduler.Default.QueueAction(continuation);
+ }
+ }
}