You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

233 lines
9.3 KiB

  1. #nullable enable
  2. using IPA.Config;
  3. using IPA.Loader;
  4. using IPA.Utilities.Async;
  5. using System;
  6. using System.Diagnostics;
  7. using System.Diagnostics.CodeAnalysis;
  8. using System.IO;
  9. using System.Reflection;
  10. using System.Runtime.CompilerServices;
  11. using System.Threading;
  12. using UnityEngine;
  13. #if NET3
  14. using Path = Net3_Proxy.Path;
  15. #endif
  16. namespace IPA.Utilities
  17. {
  18. /// <summary>
  19. /// Provides some basic utility methods and properties of Beat Saber
  20. /// </summary>
  21. public static class UnityGame
  22. {
  23. private static AlmostVersion? _gameVersion;
  24. /// <summary>
  25. /// Provides the current game version.
  26. /// </summary>
  27. /// <value>the SemVer version of the game</value>
  28. public static AlmostVersion GameVersion => _gameVersion ??= new AlmostVersion(ApplicationVersionProxy);
  29. internal static void SetEarlyGameVersion(AlmostVersion ver)
  30. {
  31. _gameVersion = ver;
  32. Logging.Logger.Default.Debug($"GameVersion set early to {ver}");
  33. }
  34. private static string ApplicationVersionProxy
  35. {
  36. [MethodImpl(MethodImplOptions.NoInlining)]
  37. get
  38. {
  39. try
  40. {
  41. return Application.version;
  42. }
  43. catch(MissingMemberException ex)
  44. {
  45. Logging.Logger.Default.Error($"Tried to grab 'Application.version' too early, it's probably broken now.");
  46. if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
  47. Logging.Logger.Default.Error(ex);
  48. }
  49. catch (Exception ex)
  50. {
  51. Logging.Logger.Default.Error($"Error getting Application.version: {ex.Message}");
  52. if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
  53. Logging.Logger.Default.Error(ex);
  54. }
  55. return string.Empty;
  56. }
  57. }
  58. internal static void EnsureRuntimeGameVersion()
  59. {
  60. try
  61. {
  62. var rtVer = new AlmostVersion(ApplicationVersionProxy);
  63. if (!rtVer.Equals(_gameVersion)) // this actually uses stricter equality than == for AlmostVersion
  64. {
  65. Logging.Logger.Default.Warn($"Early version {_gameVersion} parsed from game files doesn't match runtime version {rtVer}!");
  66. _gameVersion = rtVer;
  67. }
  68. }
  69. catch (MissingMethodException e)
  70. {
  71. Logging.Logger.Default.Error("Application.version was not found! Cannot check early parsed version");
  72. if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
  73. Logging.Logger.Default.Error(e);
  74. var st = new StackTrace();
  75. Logging.Logger.Default.Notice($"{st}");
  76. }
  77. }
  78. internal static bool IsGameVersionBoundary { get; private set; }
  79. internal static AlmostVersion? OldVersion { get; private set; }
  80. internal static void CheckGameVersionBoundary()
  81. {
  82. var gameVer = GameVersion;
  83. var lastVerS = SelfConfig.LastGameVersion_;
  84. OldVersion = lastVerS != null ? new AlmostVersion(lastVerS, gameVer) : null;
  85. IsGameVersionBoundary = OldVersion is not null && gameVer != OldVersion;
  86. SelfConfig.Instance.LastGameVersion = gameVer.ToString();
  87. }
  88. private static Thread? mainThread;
  89. /// <summary>
  90. /// Checks if the currently running code is running on the Unity main thread.
  91. /// </summary>
  92. /// <value><see langword="true"/> if the curent thread is the Unity main thread, <see langword="false"/> otherwise</value>
  93. public static bool OnMainThread => Environment.CurrentManagedThreadId == mainThread?.ManagedThreadId;
  94. /// <summary>
  95. /// Asynchronously switches the current execution context to the Unity main thread.
  96. /// </summary>
  97. /// <returns>An awaitable which causes any following code to execute on the main thread.</returns>
  98. public static SwitchToUnityMainThreadAwaitable SwitchToMainThreadAsync() => default;
  99. internal static void SetMainThread()
  100. => mainThread = Thread.CurrentThread;
  101. /// <summary>
  102. /// The different types of releases of the game.
  103. /// </summary>
  104. public enum Release
  105. {
  106. /// <summary>
  107. /// Indicates a Steam release.
  108. /// </summary>
  109. Steam,
  110. /// <summary>
  111. /// Indicates a non-Steam release.
  112. /// </summary>
  113. Other
  114. }
  115. private static Release? _releaseCache;
  116. /// <summary>
  117. /// Gets the <see cref="Release"/> type of this installation of Beat Saber
  118. /// </summary>
  119. /// <remarks>
  120. /// This only gives a
  121. /// </remarks>
  122. /// <value>the type of release this is</value>
  123. public static Release ReleaseType => _releaseCache ??= CheckIsSteam() ? Release.Steam : Release.Other;
  124. private static string? _installRoot;
  125. /// <summary>
  126. /// Gets the path to the game's install directory.
  127. /// </summary>
  128. /// <value>the path of the game install directory</value>
  129. public static string InstallPath
  130. {
  131. get
  132. {
  133. if (_installRoot == null)
  134. _installRoot = Path.GetFullPath(
  135. Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..", ".."));
  136. return _installRoot;
  137. }
  138. }
  139. /// <summary>
  140. /// The path to the `Libs` folder. Use only if necessary.
  141. /// </summary>
  142. /// <value>the path to the library directory</value>
  143. public static string LibraryPath => Path.Combine(InstallPath, "Libs");
  144. /// <summary>
  145. /// The path to the `Libs\Native` folder. Use only if necessary.
  146. /// </summary>
  147. /// <value>the path to the native library directory</value>
  148. public static string NativeLibraryPath => Path.Combine(LibraryPath, "Native");
  149. /// <summary>
  150. /// The directory to load plugins from.
  151. /// </summary>
  152. /// <value>the path to the plugin directory</value>
  153. public static string PluginsPath => Path.Combine(InstallPath, "Plugins");
  154. /// <summary>
  155. /// The path to the `UserData` folder.
  156. /// </summary>
  157. /// <value>the path to the user data directory</value>
  158. public static string UserDataPath => Path.Combine(InstallPath, "UserData");
  159. private static bool CheckIsSteam()
  160. {
  161. var installDirInfo = new DirectoryInfo(InstallPath);
  162. return installDirInfo.Parent?.Name == "common"
  163. && installDirInfo.Parent?.Parent?.Name == "steamapps";
  164. }
  165. }
  166. /// <summary>
  167. /// An awaitable which, when awaited, switches the current context to the Unity main thread.
  168. /// </summary>
  169. /// <seealso cref="UnityGame.SwitchToMainThreadAsync"/>
  170. [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types",
  171. Justification = "This type should never be compared.")]
  172. public struct SwitchToUnityMainThreadAwaitable
  173. {
  174. /// <summary>
  175. /// Gets the awaiter for this awaitable.
  176. /// </summary>
  177. /// <returns>The awaiter for this awaitable.</returns>
  178. public SwitchToUnityMainThreadAwaiter GetAwaiter() => default;
  179. }
  180. /// <summary>
  181. /// An awaiter which, when awaited, switches the current context to the Unity main thread.
  182. /// </summary>
  183. /// <seealso cref="UnityGame.SwitchToMainThreadAsync"/>
  184. [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types",
  185. Justification = "This type should never be compared.")]
  186. public struct SwitchToUnityMainThreadAwaiter : INotifyCompletion, ICriticalNotifyCompletion
  187. {
  188. private static readonly ContextCallback InvokeAction = static o => ((Action)o!)();
  189. /// <summary>
  190. /// Gets whether or not this awaiter is completed.
  191. /// </summary>
  192. public bool IsCompleted => UnityGame.OnMainThread;
  193. /// <summary>
  194. /// Gets the result of this awaiter.
  195. /// </summary>
  196. public void GetResult() { }
  197. /// <summary>
  198. /// Registers a continuation to be called when this awaiter finishes.
  199. /// </summary>
  200. /// <param name="continuation">The continuation.</param>
  201. public void OnCompleted(Action continuation)
  202. {
  203. var ec = ExecutionContext.Capture();
  204. UnityMainThreadTaskScheduler.Default.QueueAction(() => ExecutionContext.Run(ec, InvokeAction, continuation));
  205. }
  206. /// <summary>
  207. /// Registers a continuation to be called when this awaiter finishes, without capturing the execution context.
  208. /// </summary>
  209. /// <param name="continuation">The continuation.</param>
  210. public void UnsafeOnCompleted(Action continuation)
  211. {
  212. UnityMainThreadTaskScheduler.Default.QueueAction(continuation);
  213. }
  214. }
  215. }