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.

232 lines
9.3 KiB

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