RequiredSettingChecker.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Reflection;
  4. using SingularityGroup.HotReload.HarmonyLib;
  5. using UnityEditor;
  6. using UnityEditor.Compilation;
  7. using UnityEngine;
  8. namespace SingularityGroup.HotReload.Editor {
  9. using IndicationStatus = EditorIndicationState.IndicationStatus;
  10. // Before Unity 2021.3, value is 0 or 1. Only value of 1 is a problem.
  11. // From Unity 2021.3 onwards, the key is "kAutoRefreshMode".
  12. // kAutoRefreshMode options are:
  13. // 0: disabled
  14. // 1: enabled
  15. // 2: enabled outside playmode
  16. //
  17. // On newer Unity versions, Visual Studio is also checking the kAutoRefresh setting (but it should only check kAutoRefreshMode).
  18. // This is making hot reload unusable and so this setting needs to also get disabled.
  19. internal static class AutoRefreshSettingChecker {
  20. const string autoRefreshKey = "kAutoRefresh";
  21. #if UNITY_2021_3_OR_NEWER
  22. const string autoRefreshModeKey = "kAutoRefreshMode";
  23. #endif
  24. const int desiredValue = 0;
  25. public static void Apply() {
  26. if (HotReloadPrefs.AppliedAutoRefresh) {
  27. return;
  28. }
  29. var defaultPref = EditorPrefs.GetInt(autoRefreshKey);
  30. HotReloadPrefs.DefaultAutoRefresh = defaultPref;
  31. EditorPrefs.SetInt(autoRefreshKey, desiredValue);
  32. #if UNITY_2021_3_OR_NEWER
  33. var defaultModePref = EditorPrefs.GetInt(autoRefreshModeKey);
  34. HotReloadPrefs.DefaultAutoRefreshMode = defaultModePref;
  35. EditorPrefs.SetInt(autoRefreshModeKey, desiredValue);
  36. #endif
  37. HotReloadPrefs.AppliedAutoRefresh = true;
  38. }
  39. public static void Check() {
  40. if (!HotReloadPrefs.AppliedAutoRefresh) {
  41. return;
  42. }
  43. if (EditorPrefs.GetInt(autoRefreshKey) != desiredValue) {
  44. HotReloadPrefs.DefaultAutoRefresh = -1;
  45. }
  46. #if UNITY_2021_3_OR_NEWER
  47. if (EditorPrefs.GetInt(autoRefreshModeKey) != desiredValue) {
  48. HotReloadPrefs.DefaultAutoRefreshMode = -1;
  49. }
  50. #endif
  51. }
  52. public static void Reset() {
  53. if (!HotReloadPrefs.AppliedAutoRefresh) {
  54. return;
  55. }
  56. if (EditorPrefs.GetInt(autoRefreshKey) == desiredValue
  57. && HotReloadPrefs.DefaultAutoRefresh != -1
  58. ) {
  59. EditorPrefs.SetInt(autoRefreshKey, HotReloadPrefs.DefaultAutoRefresh);
  60. }
  61. HotReloadPrefs.DefaultAutoRefresh = -1;
  62. #if UNITY_2021_3_OR_NEWER
  63. if (EditorPrefs.GetInt(autoRefreshModeKey) == desiredValue
  64. && HotReloadPrefs.DefaultAutoRefreshMode != -1
  65. ) {
  66. EditorPrefs.SetInt(autoRefreshModeKey, HotReloadPrefs.DefaultAutoRefreshMode);
  67. }
  68. HotReloadPrefs.DefaultAutoRefreshMode = -1;
  69. #endif
  70. HotReloadPrefs.AppliedAutoRefresh = false;
  71. }
  72. }
  73. internal static class ScriptCompilationSettingChecker {
  74. const string scriptCompilationKey = "ScriptCompilationDuringPlay";
  75. const int recompileAndContinuePlaying = 0;
  76. static int? recompileAfterFinishedPlaying = (int?)typeof(EditorWindow).Assembly.GetType("UnityEditor.ScriptChangesDuringPlayOptions")?
  77. .GetField("RecompileAfterFinishedPlaying", BindingFlags.Static | BindingFlags.Public)?
  78. .GetValue(null);
  79. public static void Apply() {
  80. if (HotReloadPrefs.AppliedScriptCompilation) {
  81. return;
  82. }
  83. var defaultPref = EditorPrefs.GetInt(scriptCompilationKey);
  84. HotReloadPrefs.DefaultScriptCompilation = defaultPref;
  85. EditorPrefs.SetInt(scriptCompilationKey, GetRecommendedAutoScriptCompilationKey());
  86. HotReloadPrefs.AppliedScriptCompilation = true;
  87. }
  88. public static void Check() {
  89. if (!HotReloadPrefs.AppliedScriptCompilation) {
  90. return;
  91. }
  92. if (EditorPrefs.GetInt(scriptCompilationKey) != GetRecommendedAutoScriptCompilationKey()) {
  93. HotReloadPrefs.DefaultScriptCompilation = -1;
  94. }
  95. }
  96. public static void Reset() {
  97. if (!HotReloadPrefs.AppliedScriptCompilation) {
  98. return;
  99. }
  100. if (EditorPrefs.GetInt(scriptCompilationKey) == GetRecommendedAutoScriptCompilationKey()
  101. && HotReloadPrefs.DefaultScriptCompilation != -1
  102. ) {
  103. EditorPrefs.SetInt(scriptCompilationKey, HotReloadPrefs.DefaultScriptCompilation);
  104. }
  105. HotReloadPrefs.DefaultScriptCompilation = -1;
  106. HotReloadPrefs.AppliedScriptCompilation = false;
  107. }
  108. static int GetRecommendedAutoScriptCompilationKey() {
  109. // In some projects due to an unknown reason both "RecompileAndContinuePlaying" and "StopPlayingAndRecompile" cause issues
  110. // We were unable to identify the cause and therefore we always try to default to "RecompileAfterFinishedPlaying"
  111. // The exact issue users are experiencing is that domain reload happens shortly after entering play mode causing nullrefs
  112. return recompileAfterFinishedPlaying ?? recompileAndContinuePlaying;
  113. }
  114. }
  115. internal static class PlaymodeTintSettingChecker {
  116. private static readonly Color unsupportedPlaymodeColor = new Color(1f, 0.8f, 0f, 1f);
  117. private static readonly Color compilePlaymodeErrorColor = new Color(1f, 0.7f, 0.7f, 1f);
  118. public static void Apply() {
  119. if (HotReloadPrefs.AppliedEditorTint != null || !UnitySettingsHelper.I.playmodeTintSupported) {
  120. return;
  121. }
  122. var defaultPref = HotReloadPrefs.DefaultEditorTint ?? UnitySettingsHelper.I.GetCurrentPlaymodeColor();
  123. if (defaultPref == null) {
  124. return;
  125. }
  126. HotReloadPrefs.DefaultEditorTint = defaultPref.Value;
  127. var currentPlaymodeTint = GetModifiedPlaymodeTint() ?? defaultPref.Value;
  128. SetPlaymodeTint(currentPlaymodeTint);
  129. }
  130. public static void Check() {
  131. if (HotReloadPrefs.AppliedEditorTint == null || !UnitySettingsHelper.I.playmodeTintSupported) {
  132. return;
  133. }
  134. // if user modifies the settings manually, prevent the setting to be changed
  135. if (HotReloadPrefs.DefaultEditorTint == null || UnitySettingsHelper.I.GetCurrentPlaymodeColor() != HotReloadPrefs.AppliedEditorTint) {
  136. HotReloadPrefs.DefaultEditorTint = null;
  137. return;
  138. }
  139. var color = GetModifiedPlaymodeTint();
  140. if (color != null && color != HotReloadPrefs.AppliedEditorTint) {
  141. SetPlaymodeTint(color.Value);
  142. }
  143. }
  144. public static void Reset() {
  145. if (HotReloadPrefs.AppliedEditorTint == null || !UnitySettingsHelper.I.playmodeTintSupported) {
  146. return;
  147. }
  148. var color = HotReloadPrefs.DefaultEditorTint;
  149. if (color != null && UnitySettingsHelper.I.GetCurrentPlaymodeColor() == HotReloadPrefs.AppliedEditorTint) {
  150. SetPlaymodeTint(color.Value);
  151. }
  152. HotReloadPrefs.DefaultEditorTint = null;
  153. HotReloadPrefs.AppliedEditorTint = null;
  154. }
  155. private static void SetPlaymodeTint(Color color) {
  156. UnitySettingsHelper.I.SetPlaymodeTint(color);
  157. HotReloadPrefs.AppliedEditorTint = color;
  158. }
  159. private static Color? GetModifiedPlaymodeTint() {
  160. switch (EditorIndicationState.CurrentIndicationStatus) {
  161. case IndicationStatus.CompileErrors:
  162. return compilePlaymodeErrorColor;
  163. case IndicationStatus.Unsupported:
  164. return unsupportedPlaymodeColor;
  165. default:
  166. return HotReloadPrefs.DefaultEditorTint;
  167. }
  168. }
  169. }
  170. internal static class CompileMethodDetourer {
  171. static bool detouredMethod;
  172. static List<IDisposable> reverters = new List<IDisposable>();
  173. public static void Apply() {
  174. if (detouredMethod) {
  175. return;
  176. }
  177. detouredMethod = true;
  178. var originAssetRefresh = typeof(AssetDatabase).GetMethod(nameof(AssetDatabase.Refresh), Type.EmptyTypes);
  179. var targetAssetRefresh = typeof(CompileMethodDetourer).GetMethod(nameof(DetouredAssetRefresh));
  180. DetourMethod(originAssetRefresh, targetAssetRefresh);
  181. var originAssetRefreshWithParams = typeof(AssetDatabase).GetMethod(nameof(AssetDatabase.Refresh), new[] { typeof(ImportAssetOptions) });
  182. var targetAssetRefreshWithParams = typeof(CompileMethodDetourer).GetMethod(nameof(DetouredAssetRefresh));
  183. DetourMethod(originAssetRefreshWithParams, targetAssetRefreshWithParams);
  184. var originCompilation = typeof(CompilationPipeline).GetMethod(nameof(CompilationPipeline.RequestScriptCompilation), Type.EmptyTypes);
  185. var targetCompilation = typeof(CompileMethodDetourer).GetMethod(nameof(RequestScriptCompilation));
  186. DetourMethod(originCompilation, targetCompilation);
  187. }
  188. static void DetourMethod(MethodBase original, MethodBase replacement) {
  189. DetourResult result;
  190. DetourApi.DetourMethod(original, replacement, out result);
  191. if (!result.success) {
  192. Debug.LogWarning($"Detouring {original.Name} method failed. {result.exception?.GetType()} {result.exception}");
  193. } else {
  194. reverters.Add(result.patchRecord);
  195. }
  196. }
  197. public static void Reset() {
  198. if (!detouredMethod) {
  199. return;
  200. }
  201. detouredMethod = false;
  202. // don't revert for now
  203. // foreach (var reverter in reverters) {
  204. // try {
  205. // reverter.Dispose();
  206. // } catch (Exception exc) {
  207. // Debug.LogWarning($"Reverting method detour failed. {exc.GetType()} {exc}");
  208. // }
  209. // }
  210. reverters.Clear();
  211. // hack to undo changes to Editor assemblies.
  212. // Doing this when starting hotreload cancels the start
  213. // Exit playmode right away to prevent delayed compiling
  214. EditorApplication.isPlaying = false;
  215. EditorApplication.ExecuteMenuItem("Assets/Refresh");
  216. EditorUtility.RequestScriptReload(); //this will undo the modifications to the assemblies
  217. }
  218. public static void DetouredAssetRefresh(ImportAssetOptions options) { }
  219. public static void RequestScriptCompilation() { }
  220. }
  221. }