#nullable enable using IPA.Config; using IPA.Loader; using IPA.Utilities.Async; using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using UnityEngine; #if NET3 using Path = Net3_Proxy.Path; #endif namespace IPA.Utilities { /// /// Provides some basic utility methods and properties of Beat Saber /// public static class UnityGame { private static AlmostVersion? _gameVersion; /// /// Provides the current game version. /// /// the SemVer version of the game public static AlmostVersion GameVersion => _gameVersion ??= new AlmostVersion(ApplicationVersionProxy); internal static void SetEarlyGameVersion(AlmostVersion ver) { _gameVersion = ver; Logging.Logger.Default.Debug($"GameVersion set early to {ver}"); } private static string ApplicationVersionProxy { [MethodImpl(MethodImplOptions.NoInlining)] get { try { return Application.version; } catch(MissingMemberException ex) { Logging.Logger.Default.Error($"Tried to grab 'Application.version' too early, it's probably broken now."); if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) Logging.Logger.Default.Error(ex); } catch (Exception ex) { Logging.Logger.Default.Error($"Error getting Application.version: {ex.Message}"); if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) Logging.Logger.Default.Error(ex); } return string.Empty; } } internal static void EnsureRuntimeGameVersion() { try { var rtVer = new AlmostVersion(ApplicationVersionProxy); if (!rtVer.Equals(_gameVersion)) // this actually uses stricter equality than == for AlmostVersion { Logging.Logger.Default.Warn($"Early version {_gameVersion} parsed from game files doesn't match runtime version {rtVer}!"); _gameVersion = rtVer; } } catch (MissingMethodException e) { Logging.Logger.Default.Error("Application.version was not found! Cannot check early parsed version"); if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) Logging.Logger.Default.Error(e); var st = new StackTrace(); Logging.Logger.Default.Notice($"{st}"); } } internal static bool IsGameVersionBoundary { 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 is not null && gameVer != OldVersion; SelfConfig.Instance.LastGameVersion = gameVer.ToString(); } 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 => 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; /// /// The different types of releases of the game. /// public enum Release { /// /// Indicates a Steam release. /// Steam, /// /// Indicates a non-Steam release. /// Other } private static Release? _releaseCache; /// /// Gets the type of this installation of Beat Saber /// /// /// This only gives a /// /// the type of release this is public static Release ReleaseType => _releaseCache ??= CheckIsSteam() ? Release.Steam : Release.Other; private static string? _installRoot; /// /// Gets the path to the game's install directory. /// /// the path of the game install directory public static string InstallPath { get { if (_installRoot == null) _installRoot = Path.GetFullPath( Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..", "..")); return _installRoot; } } /// /// The path to the `Libs` folder. Use only if necessary. /// /// the path to the library directory public static string LibraryPath => Path.Combine(InstallPath, "Libs"); /// /// The path to the `Libs\Native` folder. Use only if necessary. /// /// the path to the native library directory public static string NativeLibraryPath => Path.Combine(LibraryPath, "Native"); /// /// The directory to load plugins from. /// /// the path to the plugin directory public static string PluginsPath => Path.Combine(InstallPath, "Plugins"); /// /// The path to the `UserData` folder. /// /// the path to the user data directory public static string UserDataPath => Path.Combine(InstallPath, "UserData"); private static bool CheckIsSteam() { var installDirInfo = new DirectoryInfo(InstallPath); return installDirInfo.Parent?.Name == "common" && 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); } } }