DESKTOP-BGJIU14\ck 7 часов назад
Родитель
Сommit
5ea935628f
100 измененных файлов с 7020 добавлено и 1 удалено
  1. 1 1
      Assets/Scripts/GameLogic/Combat/Hero/CombatHeroEntity.cs
  2. 1 0
      Assets/Scripts/GameUI/UI/CombatPanel/ZhuanPanPanel.cs
  3. 8 0
      Packages/com.singularitygroup.hotreload/Documentation.meta
  4. BIN
      Packages/com.singularitygroup.hotreload/Documentation/Documentation.pdf
  5. 7 0
      Packages/com.singularitygroup.hotreload/Documentation/Documentation.pdf.meta
  6. 8 0
      Packages/com.singularitygroup.hotreload/Editor.meta
  7. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Attribution.meta
  8. 61 0
      Packages/com.singularitygroup.hotreload/Editor/Attribution/Attribution.cs
  9. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Attribution/Attribution.cs.meta
  10. 149 0
      Packages/com.singularitygroup.hotreload/Editor/Attribution/VSAttribution.cs
  11. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Attribution/VSAttribution.cs.meta
  12. 3 0
      Packages/com.singularitygroup.hotreload/Editor/CLI.meta
  13. 128 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/CliUtils.cs
  14. 3 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/CliUtils.cs.meta
  15. 13 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/FallbackCliController.cs
  16. 3 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/FallbackCliController.cs.meta
  17. 244 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/HotReloadCli.cs
  18. 3 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/HotReloadCli.cs.meta
  19. 13 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/ICliController.cs
  20. 3 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/ICliController.cs.meta
  21. 73 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/LinuxCliController.cs
  22. 3 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/LinuxCliController.cs.meta
  23. 189 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/OsxCliController.cs
  24. 3 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/OsxCliController.cs.meta
  25. 12 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/StartArgs.cs
  26. 11 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/StartArgs.cs.meta
  27. 33 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/WindowsCliController.cs
  28. 3 0
      Packages/com.singularitygroup.hotreload/Editor/CLI/WindowsCliController.cs.meta
  29. 8 0
      Packages/com.singularitygroup.hotreload/Editor/CompileChecker.meta
  30. 70 0
      Packages/com.singularitygroup.hotreload/Editor/CompileChecker/DefaultCompileChecker.cs
  31. 11 0
      Packages/com.singularitygroup.hotreload/Editor/CompileChecker/DefaultCompileChecker.cs.meta
  32. 18 0
      Packages/com.singularitygroup.hotreload/Editor/CompileChecker/ICompileChecker.cs
  33. 11 0
      Packages/com.singularitygroup.hotreload/Editor/CompileChecker/ICompileChecker.cs.meta
  34. 55 0
      Packages/com.singularitygroup.hotreload/Editor/CompileChecker/LegacyCompileChecker.cs
  35. 11 0
      Packages/com.singularitygroup.hotreload/Editor/CompileChecker/LegacyCompileChecker.cs.meta
  36. 44 0
      Packages/com.singularitygroup.hotreload/Editor/Constants.cs
  37. 11 0
      Packages/com.singularitygroup.hotreload/Editor/Constants.cs.meta
  38. 8 0
      Packages/com.singularitygroup.hotreload/Editor/Demo.meta
  39. 26 0
      Packages/com.singularitygroup.hotreload/Editor/Demo/EditorDemo.cs
  40. 11 0
      Packages/com.singularitygroup.hotreload/Editor/Demo/EditorDemo.cs.meta
  41. 1365 0
      Packages/com.singularitygroup.hotreload/Editor/EditorCodePatcher.cs
  42. 11 0
      Packages/com.singularitygroup.hotreload/Editor/EditorCodePatcher.cs.meta
  43. 183 0
      Packages/com.singularitygroup.hotreload/Editor/EditorIndicationState.cs
  44. 3 0
      Packages/com.singularitygroup.hotreload/Editor/EditorIndicationState.cs.meta
  45. 87 0
      Packages/com.singularitygroup.hotreload/Editor/GitUtil.cs
  46. 11 0
      Packages/com.singularitygroup.hotreload/Editor/GitUtil.cs.meta
  47. 8 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers.meta
  48. 188 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/AssemblyOmission.cs
  49. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/AssemblyOmission.cs.meta
  50. 144 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/BuildInfoHelper.cs
  51. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/BuildInfoHelper.cs.meta
  52. 101 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/EditorWindowHelper.cs
  53. 11 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/EditorWindowHelper.cs.meta
  54. 162 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/GUIHelper.cs
  55. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/GUIHelper.cs.meta
  56. 544 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/HotReloadSuggestionsHelper.cs
  57. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/HotReloadSuggestionsHelper.cs.meta
  58. 606 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/HotReloadTimelineHelper.cs
  59. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/HotReloadTimelineHelper.cs.meta
  60. 80 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/Spinner.cs
  61. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/Spinner.cs.meta
  62. 95 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/UnitySettingsHelper.cs
  63. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Helpers/UnitySettingsHelper.cs.meta
  64. 35 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadAttributeProcessor.cs
  65. 11 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadAttributeProcessor.cs.meta
  66. 95 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadEventPopup.cs
  67. 3 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadEventPopup.cs.meta
  68. 178 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadOverlay.cs
  69. 3 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadOverlay.cs.meta
  70. 478 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadPrefs.cs
  71. 11 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadPrefs.cs.meta
  72. 70 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadSettingsEditor.cs
  73. 11 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadSettingsEditor.cs.meta
  74. 80 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadState.cs
  75. 3 0
      Packages/com.singularitygroup.hotreload/Editor/HotReloadState.cs.meta
  76. BIN
      Packages/com.singularitygroup.hotreload/Editor/Icon_Player.png
  77. 147 0
      Packages/com.singularitygroup.hotreload/Editor/Icon_Player.png.meta
  78. 117 0
      Packages/com.singularitygroup.hotreload/Editor/InspectorFreezeFix.cs
  79. 3 0
      Packages/com.singularitygroup.hotreload/Editor/InspectorFreezeFix.cs.meta
  80. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Installation.meta
  81. 98 0
      Packages/com.singularitygroup.hotreload/Editor/Installation/DownloadUtility.cs
  82. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Installation/DownloadUtility.cs.meta
  83. 18 0
      Packages/com.singularitygroup.hotreload/Editor/Installation/ExponentialBackoff.cs
  84. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Installation/ExponentialBackoff.cs.meta
  85. 66 0
      Packages/com.singularitygroup.hotreload/Editor/Installation/InstallUtility.cs
  86. 11 0
      Packages/com.singularitygroup.hotreload/Editor/Installation/InstallUtility.cs.meta
  87. 190 0
      Packages/com.singularitygroup.hotreload/Editor/Installation/ServerDownloader.cs
  88. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Installation/ServerDownloader.cs.meta
  89. 94 0
      Packages/com.singularitygroup.hotreload/Editor/Installation/UpdateUtility.cs
  90. 3 0
      Packages/com.singularitygroup.hotreload/Editor/Installation/UpdateUtility.cs.meta
  91. 3 0
      Packages/com.singularitygroup.hotreload/Editor/PlayerBuild.meta
  92. 42 0
      Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/BuildGenerateBuildInfo.cs
  93. 3 0
      Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/BuildGenerateBuildInfo.cs.meta
  94. 111 0
      Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/HotReloadBuildHelper.cs
  95. 3 0
      Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/HotReloadBuildHelper.cs.meta
  96. 133 0
      Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/PostbuildModifyAndroidManifest.cs
  97. 3 0
      Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/PostbuildModifyAndroidManifest.cs.meta
  98. 26 0
      Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/PostbuildSendProjectState.cs
  99. 11 0
      Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/PostbuildSendProjectState.cs.meta
  100. 60 0
      Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/PrebuildIncludeResources.cs

+ 1 - 1
Assets/Scripts/GameLogic/Combat/Hero/CombatHeroEntity.cs

@@ -312,7 +312,7 @@ public class CombatHeroEntity : ShowBaiscEntity, ITimeLineAnimtion, ITimeLineGet
         {
             return;
         }
-
+        
         long att = harmReturnInfo.att;
         IBarrier shieldsBarrier = harmReturnInfo.triggerData.IBarrier as IBarrier;
         if (shieldsBarrier != null)

+ 1 - 0
Assets/Scripts/GameUI/UI/CombatPanel/ZhuanPanPanel.cs

@@ -133,6 +133,7 @@ namespace Fort23.Mono
 
         public override void AddEvent()
         {
+            
             StaticUpdater.Instance.AddLateUpdateCallBack(Update);
             CombatEventManager.Instance.AddEventListener(CombatEventType.ExercisesAlter, ExercisesAlter);
             CombatEventManager.Instance.AddEventListener(CombatEventType.TaoismSkillAlter, TaoismSkillAlter);

+ 8 - 0
Packages/com.singularitygroup.hotreload/Documentation.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7a025eec5cd1851429c24e953a58d48b
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Packages/com.singularitygroup.hotreload/Documentation/Documentation.pdf


+ 7 - 0
Packages/com.singularitygroup.hotreload/Documentation/Documentation.pdf.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 8999c2c2d9cadcb44a617a5df023bfa1
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Packages/com.singularitygroup.hotreload/Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f5dfa6492e8e7ce4f937aa75ef4e86fd
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Attribution.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 7ae8b0adf00c450d9e80e11ffa1d2cf7
+timeCreated: 1678721517

+ 61 - 0
Packages/com.singularitygroup.hotreload/Editor/Attribution/Attribution.cs

@@ -0,0 +1,61 @@
+using System;
+using System.Globalization;
+using SingularityGroup.HotReload.DTO;
+using UnityEditor;
+using UnityEditor.VSAttribution.HotReload;
+using UnityEngine;
+using UnityEngine.Analytics;
+
+namespace SingularityGroup.HotReload.Editor {
+    internal static class Attribution {
+         internal const string LastLoginKey = "HotReload.Attribution.LastAttributionEventAt";
+         
+         //Resend attribution event every 12 hours to be safe
+         static readonly TimeSpan resendPeriod = TimeSpan.FromHours(12);
+         
+         //The last time the attribution event was sent.
+         //Returns unix epoch in case it has never been sent before.
+         static DateTime LastAttributionEventAt {
+             get {
+                 if(EditorPrefs.HasKey(LastLoginKey)) {
+                     return DateTime.ParseExact(EditorPrefs.GetString(LastLoginKey), "o", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
+                 }
+                 return DateTimeOffset.FromUnixTimeSeconds(0).UtcDateTime;
+             }
+             set {
+                 EditorPrefs.SetString(LastLoginKey, value.ToUniversalTime().ToString("o"));
+             }
+         }
+         
+         
+         const string actionName = "Login";
+         const string partnerName = "The Naughty Cult Ltd.";
+         
+         public static void RegisterLogin(LoginStatusResponse response) {
+             //Licensing might not be initialized yet.
+             //The hwId should be set eventually.
+             if(response?.hardwareId == null) {
+                 return;
+             }
+             //Only forward attribution if this is an asset store build.
+             //We will still distribute this package outside of the asset store (i.e via our website).
+             if (!PackageConst.IsAssetStoreBuild) {
+                 return;
+             }
+             
+             var now = DateTime.UtcNow;
+             //If we sent an attribution event in the last 12 hours we should already be good.
+             if (now - LastAttributionEventAt < resendPeriod) {
+                 return;
+             }
+             
+             var result = VSAttribution.SendAttributionEvent(actionName, partnerName, response.hardwareId);
+             
+             //Retry on transient errors
+             if (result == AnalyticsResult.NotInitialized) {
+                 return;
+             }
+             LastAttributionEventAt = now;
+         }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Attribution/Attribution.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 67658aafb8404f0eb9496812ba4bb8a4
+timeCreated: 1678721795

+ 149 - 0
Packages/com.singularitygroup.hotreload/Editor/Attribution/VSAttribution.cs

@@ -0,0 +1,149 @@
+using System;
+using UnityEngine.Analytics;
+
+#if UNITY_6000_0_OR_NEWER
+namespace UnityEditor.VSAttribution.HotReload
+{
+	internal static class VSAttribution
+	{
+		
+		const int k_VersionId = 4;
+		const int k_MaxEventsPerHour = 10;
+		const int k_MaxNumberOfElements = 1000;
+
+		const string k_VendorKey = "unity.vsp-attribution";
+		const string k_EventName = "vspAttribution";
+		
+		[Serializable]
+		private class VSAttributionData : IAnalytic.IData {
+			public string actionName;
+			public string partnerName;
+			public string customerUid;
+			public string extra;
+		}
+		
+		[AnalyticInfo(k_EventName, k_VendorKey, k_VersionId, k_MaxEventsPerHour, k_MaxNumberOfElements)]
+		class VSAttributionAnalytics : IAnalytic {
+			private VSAttributionData m_Data;
+
+			private VSAttributionAnalytics(
+				 string actionName,
+				 string partnerName,
+				 string customerUid,
+				 string extra
+			) {
+				this.m_Data = new VSAttributionData {
+					actionName = actionName,
+					partnerName = partnerName,
+					customerUid = customerUid,
+					extra = extra
+				};
+			}
+
+			public bool TryGatherData(out IAnalytic.IData data, out Exception error) {
+				data = this.m_Data;
+				error = null;
+				return this.m_Data != null;
+			}
+
+			public static AnalyticsResult SendEvent(
+				string actionName,
+				string partnerName,
+				string customerUid,
+				string extra
+			   ) {
+				return EditorAnalytics.SendAnalytic(new VSAttributionAnalytics(actionName,
+					partnerName,
+					customerUid,
+					extra
+				 ));
+			}
+		}
+
+		/// <summary>
+		/// Registers and attempts to send a Verified Solutions Attribution event.
+		/// </summary>
+		/// <param name="actionName">Name of the action, identifying a place this event was called from.</param>
+		/// <param name="partnerName">Identifiable Verified Solutions Partner's name.</param>
+		/// <param name="customerUid">Unique identifier of the customer using Partner's Verified Solution.</param>
+		public static AnalyticsResult SendAttributionEvent(string actionName, string partnerName, string customerUid)
+		{
+			try
+			{
+				return VSAttributionAnalytics.SendEvent(actionName, partnerName, customerUid, "{}");
+			}
+			catch
+			{
+				// Fail silently
+				return AnalyticsResult.AnalyticsDisabled;
+			}
+		}
+	}
+}
+#else
+namespace UnityEditor.VSAttribution.HotReload
+{
+	internal static class VSAttribution
+	{
+		const int k_VersionId = 4;
+		const int k_MaxEventsPerHour = 10;
+		const int k_MaxNumberOfElements = 1000;
+
+		const string k_VendorKey = "unity.vsp-attribution";
+		const string k_EventName = "vspAttribution";
+
+		static bool RegisterEvent()
+		{
+			AnalyticsResult result = EditorAnalytics.RegisterEventWithLimit(k_EventName, k_MaxEventsPerHour,
+				k_MaxNumberOfElements, k_VendorKey, k_VersionId);
+
+			var isResultOk = result == AnalyticsResult.Ok;
+			return isResultOk;
+		}
+
+		[Serializable]
+		struct VSAttributionData
+		{
+			public string actionName;
+			public string partnerName;
+			public string customerUid;
+			public string extra;
+		}
+
+		/// <summary>
+		/// Registers and attempts to send a Verified Solutions Attribution event.
+		/// </summary>
+		/// <param name="actionName">Name of the action, identifying a place this event was called from.</param>
+		/// <param name="partnerName">Identifiable Verified Solutions Partner's name.</param>
+		/// <param name="customerUid">Unique identifier of the customer using Partner's Verified Solution.</param>
+		public static AnalyticsResult SendAttributionEvent(string actionName, string partnerName, string customerUid)
+		{
+			try
+			{
+				// Are Editor Analytics enabled ? (Preferences)
+				if (!EditorAnalytics.enabled)
+					return AnalyticsResult.AnalyticsDisabled;
+
+				if (!RegisterEvent())
+					return AnalyticsResult.InvalidData;
+
+				// Create an expected data object
+				var eventData = new VSAttributionData
+				{
+					actionName = actionName,
+					partnerName = partnerName,
+					customerUid = customerUid,
+					extra = "{}"
+				};
+
+				return EditorAnalytics.SendEventWithLimit(k_EventName, eventData, k_VersionId);
+			}
+			catch
+			{
+				// Fail silently
+				return AnalyticsResult.AnalyticsDisabled;
+			}
+		}
+	}
+}
+#endif

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Attribution/VSAttribution.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d7493a30e78d4ec783ead20baea2c4d2
+timeCreated: 1678721534

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: a100625513d043c7bb875461043f4f86
+timeCreated: 1673820086

+ 128 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/CliUtils.cs

@@ -0,0 +1,128 @@
+using System.Diagnostics;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+using SingularityGroup.HotReload.Newtonsoft.Json;
+using UnityEngine;
+using System;
+
+namespace SingularityGroup.HotReload.Editor.Cli {
+    internal static class CliUtils {
+        static readonly string projectIdentifier = GetProjectIdentifier();
+
+        class Config {
+            public bool singleInstance;
+        }
+
+        public static string GetProjectIdentifier() {
+            if (File.Exists(PackageConst.ConfigFileName)) {
+                var config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFileName));
+                if (config.singleInstance) {
+                    return null;
+                }
+            }
+            var path = Path.GetDirectoryName(UnityHelper.DataPath);
+            var name = new DirectoryInfo(path).Name;
+            using (SHA256 sha256 = SHA256.Create()) {
+                byte[] inputBytes = Encoding.UTF8.GetBytes(path);
+                byte[] hashBytes = sha256.ComputeHash(inputBytes);
+                var hash = BitConverter.ToString(hashBytes).Replace("-", "").Substring(0, 6).ToUpper();
+                return $"{name}-{hash}";
+            }
+        }
+        
+        public static string GetTempDownloadFilePath(string osxFileName) {
+            if (UnityHelper.Platform == RuntimePlatform.OSXEditor) {
+                // project specific temp directory that is writeable on MacOS (Path.GetTempPath() wasn't when run through HotReload.app)
+                return Path.GetFullPath(PackageConst.LibraryCachePath + $"/HotReloadServerTemp/{osxFileName}");
+            } else {
+                return Path.GetTempFileName();
+            }
+        }
+        
+        public static string GetHotReloadTempDir() {
+            if (UnityHelper.Platform == RuntimePlatform.OSXEditor) {
+                // project specific temp directory that is writeable on MacOS (Path.GetTempPath() wasn't when run through HotReload.app)
+                return Path.GetFullPath(PackageConst.LibraryCachePath + "/HotReloadServerTemp");
+            } else {
+                if (projectIdentifier != null) {
+                    return Path.Combine(Path.GetTempPath(), "HotReloadTemp", projectIdentifier);
+                } else {
+                    return Path.Combine(Path.GetTempPath(), "HotReloadTemp");
+                }
+            }
+        }
+        
+        public static string GetAppDataPath() {
+#           if (UNITY_EDITOR_OSX)
+                var baseDir = "/Users/Shared";
+#           elif (UNITY_EDITOR_LINUX)
+                var baseDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+#           else
+                var baseDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+#endif
+            return Path.Combine(baseDir, "singularitygroup-hotreload");
+        }
+        
+        public static string GetExecutableTargetDir() {
+            if (PackageConst.IsAssetStoreBuild) {
+                return Path.Combine(GetAppDataPath(), "asset-store", $"executables_{PackageConst.ServerVersion.Replace('.', '-')}");
+            }
+            return Path.Combine(GetAppDataPath(), $"executables_{PackageConst.ServerVersion.Replace('.', '-')}");
+        }
+        
+        public static string GetCliTempDir() {
+            return Path.Combine(GetHotReloadTempDir(), "MethodPatches");
+        }
+        
+        public static void Chmod(string targetFile, string flags = "+x") {
+            // ReSharper disable once PossibleNullReferenceException
+            Process.Start(new ProcessStartInfo("chmod", $"{flags} \"{targetFile}\"") {
+                UseShellExecute = false,
+            }).WaitForExit(2000);
+        }
+        
+        public static bool TryFindServerDir(out string path) {
+            const string serverBasePath = "Packages/com.singularitygroup.hotreload/Server";
+            if(Directory.Exists(serverBasePath)) {
+                path = Path.GetFullPath(serverBasePath);
+                return true;
+            }
+            
+            //Not found in packages. Try to find in assets folder.
+            //fast path - this is the expected folder
+            const string alternativeExecutablePath = "Assets/HotReload/Server";
+            if(Directory.Exists(alternativeExecutablePath)) {
+                path = Path.GetFullPath(alternativeExecutablePath);
+                return true;
+            }
+            //slow path - try to find the server directory somewhere in the assets folder
+            var candidates = Directory.GetDirectories("Assets", "HotReload", SearchOption.AllDirectories);
+            foreach(var candidate in candidates) {
+                var serverDir = Path.Combine(candidate, "Server");
+                if(Directory.Exists(serverDir)) {
+                    path = Path.GetFullPath(serverDir);
+                    return true;
+                }
+            }
+            path = null;
+            return false;
+        }
+        
+        public static string GetPidFilePath(string hotreloadTempDir) {
+            return Path.GetFullPath(Path.Combine(hotreloadTempDir, "server.pid"));
+        }
+        
+        public static void KillLastKnownHotReloadProcess() {
+            var pidPath = GetPidFilePath(GetHotReloadTempDir());
+            try {
+                var pid = int.Parse(File.ReadAllText(pidPath));
+                Process.GetProcessById(pid).Kill();
+            }
+            catch {
+                //ignore
+            }
+            File.Delete(pidPath);
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/CliUtils.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b0243b348dec4a308dc7b98e09842d2c
+timeCreated: 1673820875

+ 13 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/FallbackCliController.cs

@@ -0,0 +1,13 @@
+
+using System.Threading.Tasks;
+
+namespace SingularityGroup.HotReload.Editor.Cli {
+    class FallbackCliController : ICliController {
+        public string BinaryFileName => "";
+        public string PlatformName => "";
+        public bool CanOpenInBackground => false;
+        public Task Start(StartArgs args) => Task.CompletedTask;
+
+        public Task Stop() => Task.CompletedTask;
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/FallbackCliController.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 090ed5d45f294f0d8799879206139bd6
+timeCreated: 1673824275

+ 244 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/HotReloadCli.cs

@@ -0,0 +1,244 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+#if UNITY_EDITOR_WIN
+using System.Net.NetworkInformation;
+#else
+using System.Net.Sockets;
+#endif
+using System.Threading.Tasks;
+using SingularityGroup.HotReload.Newtonsoft.Json;
+using UnityEditor;
+
+namespace SingularityGroup.HotReload.Editor.Cli {
+    [InitializeOnLoad]
+    public static class HotReloadCli {
+        internal static readonly ICliController controller;
+        
+        //InitializeOnLoad ensures controller gets initialized on unity thread
+        static HotReloadCli() {
+            controller =
+    #if UNITY_EDITOR_OSX
+                new OsxCliController();
+    #elif UNITY_EDITOR_LINUX
+                new LinuxCliController();
+    #elif UNITY_EDITOR_WIN
+                new WindowsCliController();
+    #else
+                new FallbackCliController();
+    #endif
+        }
+
+        public static bool CanOpenInBackground => controller.CanOpenInBackground;
+        
+        /// <summary>
+        /// Public API: Starts the Hot Reload server. Must be on the main thread
+        /// </summary>
+        public static Task StartAsync() {
+            return StartAsync(
+                isReleaseMode: RequestHelper.IsReleaseMode(),
+                exposeServerToNetwork: HotReloadPrefs.ExposeServerToLocalNetwork, 
+                allAssetChanges: HotReloadPrefs.AllAssetChanges, 
+                createNoWindow: HotReloadPrefs.DisableConsoleWindow,
+                detailedErrorReporting: !HotReloadPrefs.DisableDetailedErrorReporting
+            );
+        }
+        
+        internal static async Task StartAsync(bool exposeServerToNetwork, bool allAssetChanges, bool createNoWindow, bool isReleaseMode, bool detailedErrorReporting, LoginData loginData = null) {
+            var port = await Prepare().ConfigureAwait(false);
+            await ThreadUtility.SwitchToThreadPool();
+            StartArgs args;
+            if (TryGetStartArgs(UnityHelper.DataPath, exposeServerToNetwork, allAssetChanges, createNoWindow, isReleaseMode, detailedErrorReporting, loginData, port, out args)) {
+                await controller.Start(args);
+            }
+        }
+        
+        /// <summary>
+        /// Public API: Stops the Hot Reload server
+        /// </summary>
+        /// <remarks>
+        /// This is a no-op in case the server is not running
+        /// </remarks>
+        public static Task StopAsync() {
+            return controller.Stop();
+        }
+        
+        class Config {
+#pragma warning disable CS0649
+            public bool useBuiltInProjectGeneration;
+#pragma warning restore CS0649
+        }
+        
+        static bool TryGetStartArgs(string dataPath, bool exposeServerToNetwork, bool allAssetChanges, bool createNoWindow, bool isReleaseMode, bool detailedErrorReporting, LoginData loginData, int port, out StartArgs args) {
+            string serverDir;
+            if(!CliUtils.TryFindServerDir(out serverDir)) {
+                Log.Warning($"Failed to start the Hot Reload Server. " +
+                                 $"Unable to locate the 'Server' directory. " +
+                                 $"Make sure the 'Server' directory is " +
+                                 $"somewhere in the Assets folder inside a 'HotReload' folder or in the HotReload package");
+                args = null;
+                return false;
+            }
+            
+            Config config;
+            if (File.Exists(PackageConst.ConfigFileName)) {
+                config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFileName));
+            } else {
+                config = new Config();
+            }
+            var hotReloadTmpDir = CliUtils.GetHotReloadTempDir();
+            var cliTempDir = CliUtils.GetCliTempDir();
+            // Versioned path so that we only need to extract the binary once. User can have multiple projects
+            //  on their machine using different HotReload versions.
+            var executableTargetDir = CliUtils.GetExecutableTargetDir();
+            Directory.CreateDirectory(executableTargetDir); // ensure exists
+            var executableSourceDir = Path.Combine(serverDir, controller.PlatformName);
+            var unityProjDir = Path.GetDirectoryName(dataPath);
+            string slnPath;
+            if (config.useBuiltInProjectGeneration) {
+                var info = new DirectoryInfo(Path.GetFullPath("."));
+                slnPath = Path.Combine(Path.GetFullPath("."), info.Name + ".sln");
+                if (!File.Exists(slnPath)) {
+                    Log.Warning($"Failed to start the Hot Reload Server. Cannot find solution file. Please disable \"useBuiltInProjectGeneration\" in settings to enable custom project generation.");
+                    args = null;
+                    return false;
+                }
+                Log.Info("Using default project generation. If you encounter any problem with Unity's default project generation consider disabling it to use custom project generation.");
+                try {
+                    Directory.Delete(ProjectGeneration.ProjectGeneration.tempDir, true);
+                } catch(Exception ex) {
+                    Log.Exception(ex);
+                }
+            } else {
+                slnPath = ProjectGeneration.ProjectGeneration.GetSolutionFilePath(dataPath);
+            }
+
+            if (!File.Exists(slnPath)) {
+                Log.Warning($"No .sln file found. Open any c# file to generate it so Hot Reload can work properly");
+            }
+            
+            var searchAssemblies = string.Join(";", CodePatcher.I.GetAssemblySearchPaths());
+            var cliArguments = $@"-u ""{unityProjDir}"" -s ""{slnPath}"" -t ""{cliTempDir}"" -a ""{searchAssemblies}"" -ver ""{PackageConst.Version}"" -proc ""{Process.GetCurrentProcess().Id}"" -assets ""{allAssetChanges}"" -p ""{port}"" -r {isReleaseMode} -detailed-error-reporting {detailedErrorReporting}";
+            if (loginData != null) {
+                cliArguments += $@" -email ""{loginData.email}"" -pass ""{loginData.password}""";
+            }
+            if (exposeServerToNetwork) {
+                // server will listen on local network interface (default is localhost only)
+                cliArguments += " -e true";
+            }
+            args = new StartArgs {
+                hotreloadTempDir = hotReloadTmpDir,
+                cliTempDir = cliTempDir,
+                executableTargetDir = executableTargetDir,
+                executableSourceDir = executableSourceDir,
+                cliArguments = cliArguments,
+                unityProjDir = unityProjDir,
+                createNoWindow = createNoWindow,
+            };
+            return true;
+        }
+        
+        private static int DiscoverFreePort() {
+            var maxAttempts = 10;
+            for (int attempt = 0; attempt < maxAttempts; attempt++) {
+                var port = RequestHelper.defaultPort + attempt;
+                if (IsPortInUse(port)) {
+                    continue;
+                }
+                return port;
+            }
+            // we give up at this point
+            return RequestHelper.defaultPort + maxAttempts;
+        }
+        
+        public static bool IsPortInUse(int port) {
+        // Note that there is a racecondition that a port gets occupied after checking.
+        // However, it will very rare someone will run into this.
+#if UNITY_EDITOR_WIN
+            IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
+            IPEndPoint[] activeTcpListeners = ipGlobalProperties.GetActiveTcpListeners();
+
+            foreach (IPEndPoint endPoint in activeTcpListeners) {
+                if (endPoint.Port == port) {
+                    return true;
+                }
+            }
+
+            return false;
+#else
+            try {
+                using (TcpClient tcpClient = new TcpClient()) {
+                    tcpClient.Connect(IPAddress.Loopback, port); // Try to connect to the specified port
+                    return true;
+                }
+            } catch (SocketException) {
+                return false;
+            } catch (Exception e) {
+                Log.Exception(e);
+                // act as if the port is allocated
+                return true;
+            }
+#endif
+        }
+        
+        
+        static async Task<int> Prepare() {
+            await ThreadUtility.SwitchToMainThread();
+            
+            var dataPath = UnityHelper.DataPath;
+            await ProjectGeneration.ProjectGeneration.EnsureSlnAndCsprojFiles(dataPath);
+            await PrepareBuildInfoAsync();
+            PrepareSystemPathsFile();
+            
+            var port = DiscoverFreePort();
+            HotReloadState.ServerPort = port;
+            RequestHelper.SetServerPort(port);
+            return port;
+        }
+
+        static bool didLogWarning;
+        internal static async Task PrepareBuildInfoAsync() {
+            await ThreadUtility.SwitchToMainThread();
+            var buildInfoInput = await BuildInfoHelper.GetGenerateBuildInfoInput();
+            await Task.Run(() => {
+                try {
+                    var buildInfo = BuildInfoHelper.GenerateBuildInfoThreaded(buildInfoInput);
+                    PrepareBuildInfo(buildInfo);
+                } catch (Exception e) {
+                    if (!didLogWarning) {
+                        Log.Warning($"Preparing build info failed! On-device functionality might not work. Exception: {e}");
+                        didLogWarning = true;
+                    } else { 
+                        Log.Debug($"Preparing build info failed! On-device functionality might not work. Exception: {e}");
+                    }
+                }
+            });
+        }
+        
+        internal static void PrepareBuildInfo(BuildInfo buildInfo) {
+            // When starting server make sure it starts with correct player data state.
+            // (this fixes issue where Unity is in background and not sending files state).
+            // Always write player data because you can be on any build target and want to connect with a downloaded android build.
+            var json = buildInfo.ToJson();
+            var cliTempDir = CliUtils.GetCliTempDir();
+            Directory.CreateDirectory(cliTempDir);
+            File.WriteAllText(Path.Combine(cliTempDir, "playerdata.json"), json);
+        }
+        
+        static void PrepareSystemPathsFile() {
+#pragma warning disable CS0618 // obsolete since 2023
+            var lvl = PlayerSettings.GetApiCompatibilityLevel(EditorUserBuildSettings.selectedBuildTargetGroup);
+#pragma warning restore CS0618
+#if UNITY_2020_3_OR_NEWER
+            var dirs = UnityEditor.Compilation.CompilationPipeline.GetSystemAssemblyDirectories(lvl);
+#else
+            var t = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.Scripting.ScriptCompilation.MonoLibraryHelpers");
+            var m = t.GetMethod("GetSystemReferenceDirectories");
+            var dirs = m.Invoke(null, new object[] { lvl });
+#endif
+            Directory.CreateDirectory(PackageConst.LibraryCachePath);
+            File.WriteAllText(PackageConst.LibraryCachePath + "/systemAssemblies.json", JsonConvert.SerializeObject(dirs));
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/HotReloadCli.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 9f756ed6b78d428b8b9f83a6544317fe
+timeCreated: 1673820326

+ 13 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/ICliController.cs

@@ -0,0 +1,13 @@
+using System.Threading.Tasks;
+
+namespace SingularityGroup.HotReload.Editor.Cli {
+    interface ICliController {
+        string BinaryFileName {get;}
+        string PlatformName {get;}
+        bool CanOpenInBackground {get;}
+
+        Task Start(StartArgs args);
+        
+        Task Stop();
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/ICliController.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 8cba48e21f76483da3ba615915e731fd
+timeCreated: 1673820542

+ 73 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/LinuxCliController.cs

@@ -0,0 +1,73 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using Debug = UnityEngine.Debug;
+
+namespace SingularityGroup.HotReload.Editor.Cli {
+
+    class LinuxCliController : ICliController {
+        Process process;
+
+        public string BinaryFileName => "CodePatcherCLI";
+        public string PlatformName => "linux-x64";
+        public bool CanOpenInBackground => true;
+
+        public Task Start(StartArgs args) {
+            var startScript = Path.Combine(args.executableSourceDir, "hotreload-start-script.sh");
+            if (!File.Exists(startScript)) {
+                throw new FileNotFoundException(startScript);
+            }
+            File.WriteAllText(startScript, File.ReadAllText(startScript).Replace("\r\n", "\n"));
+            CliUtils.Chmod(startScript);
+
+            var title = CodePatcher.TAG + "Server " + new DirectoryInfo(args.unityProjDir).Name;
+            title = title.Replace(" ", "-");
+            title = title.Replace("'", "");
+
+            var cliargsfile = Path.GetTempFileName();
+            File.WriteAllText(cliargsfile,args.cliArguments);
+            var codePatcherProc = Process.Start(new ProcessStartInfo {
+                FileName = startScript,
+                Arguments =
+                    $"--title \"{title}\""
+                    + $" --executables-source-dir \"{args.executableSourceDir}\" "
+                    + $" --executable-taget-dir \"{args.executableTargetDir}\""
+                    + $" --pidfile \"{CliUtils.GetPidFilePath(args.hotreloadTempDir)}\""
+                    + $" --cli-arguments-file \"{cliargsfile}\""
+                    + $" --method-patch-dir \"{args.cliTempDir}\""
+                    + $" --create-no-window \"{args.createNoWindow}\"",
+                UseShellExecute = false,
+                RedirectStandardOutput = true,
+                RedirectStandardError = true
+            });
+            if (codePatcherProc == null) {
+                if (File.Exists(cliargsfile)) {
+                    File.Delete(cliargsfile);
+                }
+                throw new Exception("Could not start code patcher process.");
+            }
+            codePatcherProc.BeginErrorReadLine();
+            codePatcherProc.BeginOutputReadLine();
+            codePatcherProc.OutputDataReceived += (_, a) => {
+            };
+            // error data can also mean we kill the proc beningly
+            codePatcherProc.ErrorDataReceived += (_, a) => {
+            };
+            process = codePatcherProc;
+            return Task.CompletedTask;
+        }
+
+        public async Task Stop() {
+            await RequestHelper.KillServer();
+            try {
+                // process.CloseMainWindow throws if proc already exited.
+                // also we just rely on the pid file it is fine
+                CliUtils.KillLastKnownHotReloadProcess();
+            } catch {
+                //ignored
+            }
+            process = null;
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/LinuxCliController.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: c894a69d595d4ada8cfa4afe23c68ab9
+timeCreated: 1673820131

+ 189 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/OsxCliController.cs

@@ -0,0 +1,189 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using SingularityGroup.HotReload.Editor.Semver;
+using Debug = UnityEngine.Debug;
+
+namespace SingularityGroup.HotReload.Editor.Cli {
+    class OsxCliController : ICliController {
+        Process process;
+
+        public string BinaryFileName => "HotReload.app.zip";
+        public string PlatformName => "osx-x64";
+        public bool CanOpenInBackground => false;
+
+        /// In MacOS 13 Ventura, our app cannot launch a terminal window.
+        /// We use a custom app that launches HotReload server and shows it's output (just like a terminal would). 
+        //  Including MacOS 12 Monterey as well so I can dogfood it -Troy
+        private static bool UseCustomConsoleApp() => MacOSVersion.Value.Major >= 12;
+
+        // dont use static because null comparison on SemVersion is broken
+        private static readonly Lazy<SemVersion> MacOSVersion = new Lazy<SemVersion>(() => {
+            //UnityHelper.OperatingSystem; // in Unity 2018 it returns 10.16 on monterey (no idea why)
+            //Environment.OSVersion returns unix version like 21.x
+            var startinfo = new ProcessStartInfo {
+                FileName = "/usr/bin/sw_vers",
+                Arguments = "-productVersion",
+                UseShellExecute = false,
+                RedirectStandardOutput = true,
+                CreateNoWindow = true,
+            };
+            var process = Process.Start(startinfo);
+
+            string osVersion = process.StandardOutput.ReadToEnd().Trim();
+
+            SemVersion macosVersion;
+            if (SemVersion.TryParse(osVersion, out macosVersion)) {
+                return macosVersion;
+            }
+            // should never happen
+            Log.Warning("Failed to detect MacOS version, if Hot Reload fails to start, please contact support.");
+            return SemVersion.None;
+        });
+
+        public async Task Start(StartArgs args) {
+            // Unzip the .app.zip to temp folder .app
+            var appExecutablePath = $"{args.executableTargetDir}/HotReload.app/Contents/MacOS/HotReload";
+            var cliExecutablePath = $"{args.executableTargetDir}/HotReload.app/Contents/Resources/CodePatcherCLI";
+            
+            // ensure running on threadpool
+            await ThreadUtility.SwitchToThreadPool();
+
+            // executableTargetDir is versioned, so only need to extract once.
+            if (!File.Exists(appExecutablePath)) {
+                try {
+                    // delete only the extracted app folder (must not delete downloaded zip which is in same folder)
+                    Directory.Delete(args.executableTargetDir + "/HotReload.app", true);
+                } catch (IOException) {
+                    // ignore directory not found
+                }
+                Directory.CreateDirectory(args.executableTargetDir);
+                UnzipMacOsPackage($"{args.executableTargetDir}/{BinaryFileName}", args.executableTargetDir + "/");
+            }
+
+            try {
+                // Always stop first because rarely it has happened that the server process was still running after custom console closed.
+                // Note: this will also stop Hot Reload started by other Unity projects.
+                await Stop();
+            } catch {
+                // ignored
+            }
+
+            if (UseCustomConsoleApp()) {
+                await StartCustomConsole(args, appExecutablePath);
+            } else {
+                await StartTerminal(args, cliExecutablePath);
+            }
+        }
+
+        public Task StartCustomConsole(StartArgs args, string executablePath) {
+            process = Process.Start(new ProcessStartInfo {
+                // Path to the HotReload.app
+                FileName = executablePath,
+                Arguments = args.cliArguments,
+                UseShellExecute = false,
+            });
+            return Task.CompletedTask;
+        }
+
+        public Task StartTerminal(StartArgs args, string executablePath) {
+            var pidFilePath = CliUtils.GetPidFilePath(args.hotreloadTempDir);
+            // To run in a Terminal window (so you can see compiler logs), we must put the arguments into a script file
+            // and run the script in Terminal. Terminal.app does not forward the arguments passed to it via `open --args`.
+            // *.command files are opened with the user's default terminal app.
+            var executableScriptPath = Path.Combine(Path.GetTempPath(), "Start_HotReloadServer.command");
+            // You don't need to copy the cli executable on mac
+            // omit hashbang line, let shell use the default interpreter (easier than detecting your default shell beforehand)
+            File.WriteAllText(executableScriptPath, $"echo $$ > \"{pidFilePath}\"" +
+                                                    $"\ncd \"{Environment.CurrentDirectory}\"" + // set cwd because 'open' launches script with $HOME as cwd.
+                                                    $"\n\"{executablePath}\" {args.cliArguments} || read");
+
+            CliUtils.Chmod(executableScriptPath); // make it executable
+            CliUtils.Chmod(executablePath); // make it executable
+
+            Directory.CreateDirectory(args.hotreloadTempDir);
+            Directory.CreateDirectory(args.executableTargetDir);
+            Directory.CreateDirectory(args.cliTempDir);
+            
+            process = Process.Start(new ProcessStartInfo {
+                FileName = "open",
+                Arguments = $"{(args.createNoWindow ? "-gj" : "")} '{executableScriptPath}'",
+                UseShellExecute = true,
+            });
+
+            if (process.WaitForExit(1000)) {
+                if (process.ExitCode != 0) {
+                    Log.Warning("Failed to the run the start server command. ExitCode={0}\nFilepath: {1}", process.ExitCode, executableScriptPath);
+                }
+            }
+            else {
+                process.EnableRaisingEvents = true;
+                process.Exited += (_, __) => {
+                    if (process.ExitCode != 0) {
+                        Log.Warning("Failed to the run the start server command. ExitCode={0}\nFilepath: {1}", process.ExitCode, executableScriptPath);
+                    }
+                };
+            }
+            return Task.CompletedTask;
+        }
+
+        public async Task Stop() {
+            // kill HotReload server process (on mac it has different pid to the window which started it)
+            await RequestHelper.KillServer();
+
+            // process.CloseMainWindow throws if proc already exited.
+            // We rely on the pid file for killing the trampoline script (in-case script is just starting and HotReload server not running yet)
+            process = null;
+            CliUtils.KillLastKnownHotReloadProcess();
+        }
+
+        static void UnzipMacOsPackage(string zipPath, string unzippedFolderPath) {
+            //Log.Info("UnzipMacOsPackage called with {0}\n workingDirectory = {1}", zipPath, unzippedFolderPath);
+            if (!zipPath.EndsWith(".zip")) {
+                throw new ArgumentException($"Expected to end with .zip, but it was: {zipPath}", nameof(zipPath));
+            }
+
+            if (!File.Exists(zipPath)) {
+                throw new ArgumentException($"zip file not found {zipPath}", nameof(zipPath));
+            }
+            var processStartInfo = new ProcessStartInfo {
+                FileName = "unzip",
+                Arguments = $"-o \"{zipPath}\"",
+                WorkingDirectory = unzippedFolderPath, // unzip extracts to working directory by default
+                UseShellExecute = true,
+                CreateNoWindow = true
+            };
+
+            Process process = Process.Start(processStartInfo);
+            process.WaitForExit();
+            if (process.ExitCode != 0) {
+                throw new Exception($"unzip failed with ExitCode {process.ExitCode}");
+            }
+            //Log.Info($"did unzip to {unzippedFolderPath}");
+            // Move the .app folder to unzippedFolderPath
+            
+            // find the .app directory which is now inside unzippedFolderPath directory
+            var foundDirs = Directory.GetDirectories(unzippedFolderPath, "*.app", SearchOption.AllDirectories);
+            var done = false;
+            var destDir = unzippedFolderPath + "HotReload.app";
+            foreach (var dir in foundDirs) {
+                if (dir.EndsWith(".app")) {
+                    done = true;
+                    if (dir == destDir) {
+                        // already in the right place
+                        break;
+                    }
+                    Directory.Move(dir, destDir);
+                    //Log.Info("Moved to " + destDir);
+                    break;
+                }
+            }
+
+            if (!done) {
+                throw new Exception("Failed to find .app directory and move it to " + destDir);
+            }
+            //Log.Info($"did unzip to {unzippedFolderPath}");
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/OsxCliController.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 5ebeed1c29454bc78e5a9ee64f2c9def
+timeCreated: 1673821666

+ 12 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/StartArgs.cs

@@ -0,0 +1,12 @@
+namespace SingularityGroup.HotReload.Editor.Cli {
+    class StartArgs {
+        public string hotreloadTempDir;
+        // aka method patch temp dir
+        public string cliTempDir;
+        public string executableTargetDir;
+        public string executableSourceDir;
+        public string cliArguments;
+        public string unityProjDir;
+        public bool createNoWindow;
+    }
+}

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/StartArgs.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 43d69eb7ae8aef4428da83562105bfaa
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 33 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/WindowsCliController.cs

@@ -0,0 +1,33 @@
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace SingularityGroup.HotReload.Editor.Cli {
+    class WindowsCliController : ICliController {
+        Process process;
+
+        public string BinaryFileName => "CodePatcherCLI.exe";
+        public string PlatformName => "win-x64";
+        public bool CanOpenInBackground => true;
+
+        public Task Start(StartArgs args) {
+            process = Process.Start(new ProcessStartInfo {
+                FileName = Path.GetFullPath(Path.Combine(args.executableTargetDir, "CodePatcherCLI.exe")),
+                Arguments = args.cliArguments,
+                UseShellExecute = !args.createNoWindow,
+                CreateNoWindow = args.createNoWindow,
+            });
+            return Task.CompletedTask;
+        }
+
+        public async Task Stop() {
+            await RequestHelper.KillServer();
+            try {
+                process?.CloseMainWindow();
+            } catch {
+                //ignored
+            }  
+            process = null;
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/CLI/WindowsCliController.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e5644af69ec7404a8039ff2833610d48
+timeCreated: 1673822169

+ 8 - 0
Packages/com.singularitygroup.hotreload/Editor/CompileChecker.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 80c2056f805745542a2c295385b25479
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 70 - 0
Packages/com.singularitygroup.hotreload/Editor/CompileChecker/DefaultCompileChecker.cs

@@ -0,0 +1,70 @@
+#if UNITY_2019_1_OR_NEWER
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using UnityEditor;
+using UnityEditor.Compilation;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+    class DefaultCompileChecker : ICompileChecker {
+        const string recompileFilePath = PackageConst.LibraryCachePath + "/recompile.txt";
+        public bool hasCompileErrors { get; private set;  }
+        bool recompile;
+        public DefaultCompileChecker() {
+            CompilationPipeline.assemblyCompilationFinished += DetectCompileErrors;
+            CompilationPipeline.compilationFinished += OnCompilationFinished;
+            var currentSessionId = EditorAnalyticsSessionInfo.id;
+            Task.Run(() => {
+                try {
+                    var compileSessionId = File.ReadAllText(recompileFilePath);
+                    if(compileSessionId == currentSessionId.ToString()) {
+                        ThreadUtility.RunOnMainThread(() => {
+                            recompile = true;
+                            _onCompilationFinished?.Invoke();
+                        });
+                    }
+                    File.Delete(recompileFilePath);
+                } catch(DirectoryNotFoundException) {
+                   //dir doesn't exist -> no recompile required
+                } catch(FileNotFoundException) {
+                   //file doesn't exist -> no recompile required
+                } catch(Exception ex) {
+                    Log.Warning("compile checker encountered issue: {0} {1}", ex.GetType().Name, ex.Message);
+                }
+            });
+        }
+        
+        void DetectCompileErrors(string _, CompilerMessage[] messages) {
+            for (int i = 0; i < messages.Length; i++) {
+                if (messages[i].type == CompilerMessageType.Error) {
+                    hasCompileErrors = true;
+                    return;
+                }
+            }
+            hasCompileErrors = false;
+        }
+
+        void OnCompilationFinished(object _) {
+            //Don't recompile on compile errors
+            if(!hasCompileErrors) {
+                Directory.CreateDirectory(Path.GetDirectoryName(recompileFilePath));
+                File.WriteAllText(recompileFilePath, EditorAnalyticsSessionInfo.id.ToString());
+            }
+        }
+
+        Action _onCompilationFinished;
+        public event Action onCompilationFinished {
+            add {
+                if(recompile && value != null) {
+                    value();
+                }
+                _onCompilationFinished += value;
+            }
+            remove {
+                _onCompilationFinished -= value;
+            }
+        }
+    }
+}
+#endif

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/CompileChecker/DefaultCompileChecker.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ab09f7c657e6ecb44b65dd9f8cfc3d9f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 18 - 0
Packages/com.singularitygroup.hotreload/Editor/CompileChecker/ICompileChecker.cs

@@ -0,0 +1,18 @@
+using System;
+
+namespace SingularityGroup.HotReload.Editor {
+    interface ICompileChecker {
+        event Action onCompilationFinished;
+        bool hasCompileErrors { get; }
+    }
+    
+    static class CompileChecker {
+        internal static ICompileChecker Create() {
+            #if UNITY_2019_1_OR_NEWER
+                return new DefaultCompileChecker();
+            #else
+                return new LegacyCompileChecker();
+            #endif
+        }
+    }
+}

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/CompileChecker/ICompileChecker.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 82bf36f2126bbd1498d4964272426e0f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 55 - 0
Packages/com.singularitygroup.hotreload/Editor/CompileChecker/LegacyCompileChecker.cs

@@ -0,0 +1,55 @@
+#if !UNITY_2019_1_OR_NEWER
+using System;
+using System.Globalization;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace SingularityGroup.HotReload.Editor {
+    class LegacyCompileChecker : ICompileChecker {
+        const string timestampFilePath = PackageConst.LibraryCachePath + "/lastCompileTimestamp.txt";
+        public bool hasCompileErrors { get; }
+        const string assemblyPath = "Library/ScriptAssemblies";
+        bool recompile;
+        public LegacyCompileChecker() {
+            Task.Run(() => {
+                var info = new DirectoryInfo(assemblyPath);
+                if(!info.Exists) {
+                    return;
+                }
+                var currentCompileTimestamp = default(DateTime);
+                foreach (var file in info.GetFiles("*.dll")) {
+                    var fileWriteDate = file.LastWriteTimeUtc;
+                    if(fileWriteDate > currentCompileTimestamp) {
+                        currentCompileTimestamp = fileWriteDate;
+                    }
+                }
+                if(File.Exists(timestampFilePath)) {
+                    var lastTimestampStr = File.ReadAllText(timestampFilePath);
+                    var lastTimestamp = DateTime.ParseExact(lastTimestampStr, "o", CultureInfo.CurrentCulture).ToUniversalTime();
+                    if(currentCompileTimestamp > lastTimestamp) {
+                        ThreadUtility.RunOnMainThread(() => {
+                            recompile = true;
+                            _onCompilationFinished?.Invoke();
+                        });
+                    }
+                }
+                Directory.CreateDirectory(Path.GetDirectoryName(timestampFilePath));
+                File.WriteAllText(timestampFilePath, currentCompileTimestamp.ToString("o"));
+            });
+        }
+
+        Action _onCompilationFinished;
+        public event Action onCompilationFinished {
+            add {
+                if(recompile && value != null) {
+                    value();
+                }
+                _onCompilationFinished += value;
+            }
+            remove {
+                _onCompilationFinished -= value;
+            }
+        }
+    }
+}
+#endif

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/CompileChecker/LegacyCompileChecker.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f56ec68ce4b1fcc4b9c8ba5962d890f1
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 44 - 0
Packages/com.singularitygroup.hotreload/Editor/Constants.cs

@@ -0,0 +1,44 @@
+
+namespace SingularityGroup.HotReload.Editor {
+    internal static class Constants {
+        public const string WebsiteURL = "https://hotreload.net";
+
+        public const string ProductPurchaseURL = WebsiteURL + "/pricing";
+        public const string ProductPurchaseBusinessURL = ProductPurchaseURL + "?tab=business";
+        public const string DocumentationURL = WebsiteURL + "/documentation";
+        public const string AdditionalContentURL = DocumentationURL + "/getting-started#downloading-additional-content";
+        public const string DownloadUrl = WebsiteURL + "/download";
+        public const string ContactURL = WebsiteURL + "/contact";
+        public const string ForumURL = "https://forum.unity.com/threads/hot-reload-edit-code-without-compiling.1389969/";
+        public const string ManageLicenseURL = "https://billing.stripe.com/p/login/28odTObUQ0CU0Za3cc";
+        public const string ManageAccountURL = "https://users.licensespring.com/login";
+        public const string ForgotPasswordURL = "https://users.licensespring.com/reset-password";
+        public const string ReportIssueURL = "https://gitlab.com/singularitygroup/hot-reload-for-unity/-/issues/new";
+        public const string TroubleshootingURL = "https://hotreload.net/documentation/troubleshooting";
+        public const string RecompileTroubleshootingURL = TroubleshootingURL + "#unity-recompiles-every-time-i-enterexit-playmode";
+        public const string FeaturesDocumentationURL = DocumentationURL + "/features";
+        public const string MultipleEditorsURL = DocumentationURL + "/multiple-editors";
+        public const string DebuggerURL = DocumentationURL + "/debugger";
+        public const string UndetectedChangesURL = DocumentationURL + "/getting-started#undetected-changes";
+        public const string VoteForAwardURL = "https://awards.unity.com/#best-development-tool";
+        public const string UnityStoreRateAppURL = "https://assetstore.unity.com/packages/slug/254358#reviews";
+        public const string ChangelogURL = WebsiteURL + "/changelog";
+        public const string DiscordInviteUrl = "https://discord.com/invite/kgxAS4Bqxr";
+        
+        public const int DaysToRateApp = 5;
+        public const int RecompileButtonTextHideWidth = 460;
+        public const int IndicationTextHideWidth = 360;
+        public const int StartButtonTextHideWidth = 400;
+        public const int EventsListHideHeight = 360;
+        public const int EventsListHideWidth = 425;
+        public const int UpgradeLicenseNoteHideWidth = 325;
+        public const int UpgradeLicenseNoteHideHeight = 150;
+        public const int RateAppHideHeight = 325;
+        public const int RateAppHideWidth = 300;
+        public const int EventFiltersShownHideWidth = 275;
+        public const int ConsumptionsHideWidth = 300;
+        public const int ConsumptionsHideHeight = 360;
+        
+        public const string Only40EntriesShown = "Only last 40 entries are shown";
+    }
+}

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/Constants.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ce502822e7fa34844bcb385f44091eb9
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Packages/com.singularitygroup.hotreload/Editor/Demo.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7c5c2596a7a469c42a1a6b35017d8a49
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 26 - 0
Packages/com.singularitygroup.hotreload/Editor/Demo/EditorDemo.cs

@@ -0,0 +1,26 @@
+using System.Collections;
+using System.IO;
+using SingularityGroup.HotReload.Demo;
+using UnityEditor;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor.Demo {
+    class EditorDemo : IDemo {
+        public bool IsServerRunning() {
+            return ServerHealthCheck.I.IsServerHealthy;
+        }
+
+        public void OpenHotReloadWindow() {
+            HotReloadWindow.Open();
+        }
+
+        public void OpenScriptFile(TextAsset textAsset, int line, int column) {
+            var path = Path.GetFullPath(AssetDatabase.GetAssetPath(textAsset));
+#if UNITY_2019_4_OR_NEWER
+            Unity.CodeEditor.CodeEditor.CurrentEditor.OpenProject(path, line, column);
+#else
+            EditorUtility.OpenWithDefaultApp(path);
+#endif
+        }
+    }
+}

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/Demo/EditorDemo.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fde6b5b57a3aeba4888a7bdaa16b3074
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 1365 - 0
Packages/com.singularitygroup.hotreload/Editor/EditorCodePatcher.cs

@@ -0,0 +1,1365 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using SingularityGroup.HotReload.DTO;
+using SingularityGroup.HotReload.Editor.Cli;
+using SingularityGroup.HotReload.Editor.Demo;
+using SingularityGroup.HotReload.EditorDependencies;
+using SingularityGroup.HotReload.RuntimeDependencies;
+using UnityEditor;
+using UnityEngine;
+using Debug = UnityEngine.Debug;
+using Task = System.Threading.Tasks.Task;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using SingularityGroup.HotReload.Newtonsoft.Json;
+using SingularityGroup.HotReload.ZXing;
+using UnityEditor.Compilation;
+using UnityEditor.UIElements;
+using UnityEditorInternal;
+using UnityEngine.UIElements;
+
+[assembly: InternalsVisibleTo("SingularityGroup.HotReload.IntegrationTests")]
+
+namespace SingularityGroup.HotReload.Editor {
+    internal class Config {
+        public bool patchEditModeOnlyOnEditorFocus;
+        public string[] assetBlacklist;
+        public bool changePlaymodeTint;
+        public bool disableCompilingFromEditorScripts;
+        public bool enableInspectorFreezeFix;
+    }
+    
+    [InitializeOnLoad]
+    internal static class EditorCodePatcher {
+        const string sessionFilePath = PackageConst.LibraryCachePath + "/sessionId.txt";
+        const string patchesFilePath = PackageConst.LibraryCachePath + "/patches.json";
+        
+        internal static readonly ServerDownloader serverDownloader;
+        internal static bool _compileError;
+        internal static bool _applyingFailed;
+        internal static bool _appliedPartially;
+        internal static bool _appliedUndetected;
+        
+        static Timer timer; 
+        static bool init;
+
+        internal static UnityLicenseType licenseType { get; private set; }
+        internal static bool LoginNotRequired => PackageConst.IsAssetStoreBuild && licenseType != UnityLicenseType.UnityPro;
+        internal static bool compileError => _compileError;
+        
+        internal static PatchStatus patchStatus = PatchStatus.None;
+        
+        internal static event Action<(MethodPatchResponse, RegisterPatchesResult)> OnPatchHandled;
+        
+        
+        internal static Config config;
+
+        
+        #if ODIN_INSPECTOR
+        internal static bool DrawPrefix(Sirenix.OdinInspector.Editor.InspectorProperty __instance) {
+            return !UnityFieldHelper.IsFieldHidden(__instance.ParentType, __instance.Name);
+        }
+        internal static MethodInfo OdinPropertyDrawPrefixInfo = typeof(EditorCodePatcher).GetMethod("DrawPrefix", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
+        #if UNITY_2021_1_OR_NEWER
+        internal static MethodInfo OdinPropertyDrawInfo = typeof(Sirenix.OdinInspector.Editor.InspectorProperty)?.GetMethod("Draw", 0, BindingFlags.Instance | BindingFlags.Public, null, new Type[]{}, null);
+        #else
+        internal static MethodInfo OdinPropertyDrawInfo = typeof(Sirenix.OdinInspector.Editor.InspectorProperty)?.GetMethod("Draw", BindingFlags.Instance | BindingFlags.Public, null, new Type[]{}, null);
+        #endif
+        internal static MethodInfo DrawOdinInspectorInfo = typeof(Sirenix.OdinInspector.Editor.OdinEditor)?.GetMethod("DrawOdinInspector", BindingFlags.NonPublic | BindingFlags.Instance);
+        #else
+        internal static MethodInfo OdinPropertyDrawPrefixInfo = null;
+        internal static MethodInfo OdinPropertyDrawInfo = null;
+        internal static MethodInfo DrawOdinInspectorInfo = null;
+        #endif
+
+        internal static MethodInfo GetDrawVInspectorInfo() {
+            // performance optimization
+            if (!Directory.Exists("Assets/vInspector")) {
+                return null;
+            }
+            try {
+                var t = Type.GetType("VInspector.AbstractEditor, VInspector");
+                return t?.GetMethod("OnInspectorGUI", BindingFlags.Public | BindingFlags.Instance);
+            } catch {
+                // ignore
+            }
+            return null;
+        }
+
+        internal static ICompileChecker compileChecker;
+        static bool quitting;
+        static EditorCodePatcher() {
+            if(init) {
+                //Avoid infinite recursion in case the static constructor gets accessed via `InitPatchesBlocked` below
+                return;
+            }
+            if (File.Exists(PackageConst.ConfigFileName)) {
+                config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFileName));
+            } else {
+                config = new Config();
+            }
+            init = true;
+            UnityHelper.Init();
+            //Use synchonization context if possible because it's more reliable.
+            ThreadUtility.InitEditor();
+            if (!EditorWindowHelper.IsHumanControllingUs()) {
+                return;
+            }
+            
+            serverDownloader = new ServerDownloader();
+            serverDownloader.CheckIfDownloaded(HotReloadCli.controller);
+            SingularityGroup.HotReload.Demo.Demo.I = new EditorDemo();
+            if (HotReloadPrefs.DeactivateHotReload || new DirectoryInfo(Path.GetFullPath("..")).Name == "VP") {
+                ResetSettings();
+                return;
+            }
+            
+            // ReSharper disable ExpressionIsAlwaysNull
+            UnityFieldHelper.Init(Log.Warning, HotReloadRunTab.Recompile, DrawOdinInspectorInfo, OdinPropertyDrawInfo, OdinPropertyDrawPrefixInfo, GetDrawVInspectorInfo(), typeof(UnityFieldDrawerPatchHelper));
+            
+            timer = new Timer(OnIntervalThreaded, (Action) OnIntervalMainThread, 500, 500);
+
+            UpdateHost();
+            licenseType = UnityLicenseHelper.GetLicenseType();
+            compileChecker = CompileChecker.Create();
+            compileChecker.onCompilationFinished += OnCompilationFinished;
+            EditorApplication.delayCall += InstallUtility.CheckForNewInstall;
+            AddEditorFocusChangedHandler(OnEditorFocusChanged);
+            // When domain reloads, this is a good time to ensure server has up-to-date project information
+            if (ServerHealthCheck.I.IsServerHealthy) {
+                EditorApplication.delayCall += TryPrepareBuildInfo;
+            }
+            HotReloadSuggestionsHelper.Init();
+            // reset in case last session didn't shut down properly
+            CheckEditorSettings();
+            EditorApplication.quitting += ResetSettingsOnQuit;
+            
+            AssemblyReloadEvents.beforeAssemblyReload += () => {
+                HotReloadTimelineHelper.PersistTimeline();
+            };
+            
+            CompilationPipeline.compilationFinished += obj => {
+                // reset in case package got removed
+                // if it got removed, it will not be enabled again
+                // if it wasn't removed, settings will get handled by OnIntervalMainThread
+                AutoRefreshSettingChecker.Reset();
+                ScriptCompilationSettingChecker.Reset();
+                PlaymodeTintSettingChecker.Reset();
+                HotReloadRunTab.recompiling = false;
+                CompileMethodDetourer.Reset();
+            };
+            DetectEditorStart();
+            DetectVersionUpdate();
+            CodePatcher.I.fieldHandler = new FieldHandler(FieldDrawerUtil.StoreField, UnityFieldHelper.HideField, UnityFieldHelper.RegisterInspectorFieldAttributes);
+            if (EditorApplication.isPlayingOrWillChangePlaymode) {
+                CodePatcher.I.InitPatchesBlocked(patchesFilePath);
+                HotReloadTimelineHelper.InitPersistedEvents();
+            }
+
+#pragma warning disable CS0612 // Type or member is obsolete
+            if (HotReloadPrefs.RateAppShownLegacy) {
+                HotReloadPrefs.RateAppShown = true;
+            }
+            if (!File.Exists(HotReloadPrefs.showOnStartupPath)) {
+                var showOnStartupLegacy = HotReloadPrefs.GetShowOnStartupEnum();
+                HotReloadPrefs.ShowOnStartup = showOnStartupLegacy;
+            }
+#pragma warning restore CS0612 // Type or member is obsolete
+            
+            HotReloadState.ShowingRedDot = false;
+
+            if (DateTime.Now < new DateTime(2023, 11, 1)) {
+                HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023);
+            } else {
+                HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023);
+            }
+            
+            EditorApplication.playModeStateChanged += state => {
+                if (state == PlayModeStateChange.EnteredEditMode && HotReloadPrefs.AutoRecompileUnsupportedChangesOnExitPlayMode) {
+                    if (TryRecompileUnsupportedChanges()) {
+                        HotReloadState.RecompiledUnsupportedChangesOnExitPlaymode = true;
+                    }
+                }
+            };
+            if (HotReloadState.RecompiledUnsupportedChangesInPlaymode) {
+                HotReloadState.RecompiledUnsupportedChangesInPlaymode = false;
+                EditorApplication.isPlaying = true;
+            }
+#if UNITY_2020_1_OR_NEWER
+            if (CompilationPipeline.codeOptimization != CodeOptimization.Release) {
+                HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods);
+            }
+#endif
+            if (!HotReloadState.EditorCodePatcherInit) {
+                ClearPersistence();
+                HotReloadState.EditorCodePatcherInit = true;
+            }
+
+            CodePatcher.I.debuggerCompatibilityEnabled = !HotReloadPrefs.AutoDisableHotReloadWithDebugger;
+        }
+
+        static void ResetSettingsOnQuit() {
+            quitting = true;
+            ResetSettings();
+        }
+        
+        static void ResetSettings() {
+            AutoRefreshSettingChecker.Reset();
+            ScriptCompilationSettingChecker.Reset();
+            PlaymodeTintSettingChecker.Reset();
+            HotReloadCli.StopAsync().Forget();
+            CompileMethodDetourer.Reset();
+        }
+
+        public static bool autoRecompileUnsupportedChangesSupported;
+        static void AddEditorFocusChangedHandler(Action<bool> handler) {
+            var eventInfo = typeof(EditorApplication).GetEvent("focusChanged", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
+            var addMethod = eventInfo?.GetAddMethod(true) ?? eventInfo?.GetAddMethod(false);
+            if (addMethod != null) {
+                addMethod.Invoke(null, new object[]{ handler });
+            }
+            autoRecompileUnsupportedChangesSupported = addMethod != null;
+        }
+
+        private static void OnEditorFocusChanged(bool hasFocus) {
+            if (hasFocus && !HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately) {
+                TryRecompileUnsupportedChanges();
+            }
+        }
+
+        public static bool TryRecompileUnsupportedChanges() {
+            var isPlaying = EditorApplication.isPlaying;
+            if (!HotReloadPrefs.AutoRecompileUnsupportedChanges
+                || HotReloadTimelineHelper.UnsupportedChangesCount == 0
+                    && (!HotReloadPrefs.AutoRecompilePartiallyUnsupportedChanges || HotReloadTimelineHelper.PartiallySupportedChangesCount == 0)
+                || _compileError 
+                || isPlaying && !HotReloadPrefs.AutoRecompileUnsupportedChangesInPlayMode
+            ) {
+                return false;
+            }
+
+            if (HotReloadPrefs.ShowCompilingUnsupportedNotifications) {
+                EditorWindowHelper.ShowNotification(EditorWindowHelper.NotificationStatus.NeedsRecompile);
+            }
+            if (isPlaying) {
+                HotReloadState.RecompiledUnsupportedChangesInPlaymode = true;
+            }
+            HotReloadRunTab.Recompile();
+            return true;
+        }
+
+        private static DateTime lastPrepareBuildInfo = DateTime.UtcNow;
+
+        /// Post state for player builds.
+        /// Only check build target because user can change build settings whenever.
+        internal static void TryPrepareBuildInfo() {
+            // Note: we post files state even when build target is wrong
+            // because you might connect with a build downloaded onto the device. 
+            if ((DateTime.UtcNow - lastPrepareBuildInfo).TotalSeconds > 5) {
+                lastPrepareBuildInfo = DateTime.UtcNow;
+                HotReloadCli.PrepareBuildInfoAsync().Forget();
+            }
+        }
+
+        internal static void RecordActiveDaysForRateApp() {
+            var unixDay = (int)(DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 86400);
+            var activeDays = GetActiveDaysForRateApp();
+            if (activeDays.Count < Constants.DaysToRateApp && activeDays.Add(unixDay.ToString())) {
+                HotReloadPrefs.ActiveDays = string.Join(",", activeDays);
+            }
+        }
+        
+        internal static HashSet<string> GetActiveDaysForRateApp() {
+            if (string.IsNullOrEmpty(HotReloadPrefs.ActiveDays)) {
+                return new HashSet<string>();
+            }
+            return new HashSet<string>(HotReloadPrefs.ActiveDays.Split(','));
+        }
+
+        // CheckEditorStart distinguishes between domain reload and first editor open
+        // We have some separate logic on editor start (InstallUtility.HandleEditorStart)
+        private static void DetectEditorStart() {
+            var editorId = EditorAnalyticsSessionInfo.id;
+            var currVersion = PackageConst.Version;
+            Task.Run(() => {
+                try {
+                    var lines = File.Exists(sessionFilePath) ? File.ReadAllLines(sessionFilePath) : Array.Empty<string>();
+
+                    long prevSessionId = -1;
+                    string prevVersion = null;
+                    if (lines.Length >= 2) {
+                        long.TryParse(lines[1], out prevSessionId);
+                    }
+                    if (lines.Length >= 3) {
+                        prevVersion = lines[2].Trim();
+                    }
+                    var updatedFromVersion = (prevSessionId != -1 && currVersion != prevVersion) ? prevVersion : null;
+
+                    if (prevSessionId != editorId && prevSessionId != 0) {
+                        // back to mainthread
+                        ThreadUtility.RunOnMainThread(() => {
+                            InstallUtility.HandleEditorStart(updatedFromVersion);
+
+                            var newEditorId = EditorAnalyticsSessionInfo.id;
+                            if (newEditorId != 0) {
+                                Task.Run(() => {
+                                    try {
+                                        // editorId isn't available on first domain reload, must do it here
+                                        File.WriteAllLines(sessionFilePath, new[] {
+                                            "1", // serialization version
+                                            newEditorId.ToString(),
+                                            currVersion,
+                                        });
+
+                                    } catch (IOException) {
+                                        // ignore
+                                    }
+                                });
+                            }
+                        });
+                    }
+
+                } catch (IOException) {
+                    // ignore
+                } catch (Exception e) {
+                    ThreadUtility.LogException(e);
+                }
+            });
+        }
+        
+        private static void DetectVersionUpdate() {
+            if (serverDownloader.CheckIfDownloaded(HotReloadCli.controller)) {
+                return;
+            }
+            ServerHealthCheck.instance.CheckHealth();
+            if (!ServerHealthCheck.I.IsServerHealthy) {
+                return;
+            }
+            var restartServer = EditorUtility.DisplayDialog("Hot Reload",
+                $"When updating Hot Reload, the server must be restarted for the update to take effect." +
+                "\nDo you want to restart it now?",
+                "Restart server", "Don't restart");
+            if (restartServer) {
+                RestartCodePatcher().Forget();
+            }
+        }
+
+        private static void UpdateHost() {
+            RequestHelper.SetServerInfo(new PatchServerInfo(RequestHelper.defaultServerHost, HotReloadState.ServerPort, null, Path.GetFullPath(".")));
+        }
+
+        static void OnIntervalThreaded(object o) {
+            ServerHealthCheck.instance.CheckHealth();
+            ThreadUtility.RunOnMainThread((Action)o);
+            if (serverDownloader.Progress >= 1f) {
+                serverDownloader.CheckIfDownloaded(HotReloadCli.controller);
+            }
+        }
+
+        private static bool _requestingFlushErrors;
+        private static long _lastErrorFlush;
+        private static async Task RequestFlushErrors() {
+            _requestingFlushErrors = true;
+            try {
+                await RequestFlushErrorsCore();
+            } finally {
+                _requestingFlushErrors = false;
+            }
+        }
+        
+        private static async Task RequestFlushErrorsCore() {
+            var pollFrequency = 500;
+            // Delay until we've hit the poll request frequency
+            var waitMs = (int)Mathf.Clamp(pollFrequency - ((DateTime.Now.Ticks / (float)TimeSpan.TicksPerMillisecond) - _lastErrorFlush), 0, pollFrequency);
+            await Task.Delay(waitMs);
+            await FlushErrors();
+            _lastErrorFlush = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
+        }
+
+        public static bool disableServerLogs;
+        public static string lastCompileErrorLog;
+        static async Task FlushErrors() {
+            var response = await RequestHelper.RequestFlushErrors();
+            if (response == null || disableServerLogs) {
+                return;
+            }
+            foreach (var responseWarning in response.warnings) {
+                if (responseWarning.Contains("Scripts have compile errors")) {
+                    if (compileError) {
+                        Log.Error(responseWarning);
+                    } else {
+                        lastCompileErrorLog = responseWarning;
+                    }
+                } else {
+                    Log.Warning(responseWarning);
+                }
+
+                if (responseWarning.Contains("Multidimensional arrays are not supported")) {
+                    await ThreadUtility.SwitchToMainThread();
+                    HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.MultidimensionalArrays);
+                }
+            }
+            foreach (var responseError in response.errors) {
+                Log.Error(responseError);
+            }
+        }
+        
+        internal static bool firstPatchAttempted;
+        internal static bool loggedDebuggerRecompile;
+        static void OnIntervalMainThread() {
+            HotReloadSuggestionsHelper.Check();
+            
+            // Moved from RequestServerInfo to avoid GC allocations when HR is not active
+            
+            // Repaint if the running Status has changed since the layout changes quite a bit
+            if (running != ServerHealthCheck.I.IsServerHealthy) {
+                if (HotReloadWindow.Current) {
+                    HotReloadRunTab.RepaintInstant();
+                }
+                running = ServerHealthCheck.I.IsServerHealthy;
+            }
+            if (!running) {
+                startupCompletedAt = null;
+            }
+            if (!running && !StartedServerRecently()) {
+                // Reset startup progress
+                startupProgress = null;
+            }
+
+            if (HotReloadPrefs.AutoDisableHotReloadWithDebugger && Debugger.IsAttached) {
+                if (!HotReloadState.ShowedDebuggerCompatibility) {
+                    HotReloadSuggestionsHelper.SetSuggestionActive(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
+                    HotReloadState.ShowedDebuggerCompatibility = true;
+                }
+                if (CodePatcher.I.OriginalPatchMethods.Count() > 0) {
+                    if (!Application.isPlaying) {
+                        if (!loggedDebuggerRecompile) {
+                            Log.Info("Debugger was attached. Hot Reload may interfere with your debugger session. Recompiling in order to get full debugger experience.");
+                            loggedDebuggerRecompile = true;
+                        }
+                        HotReloadRunTab.Recompile();
+                        HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached);
+                    } else {
+                        HotReloadSuggestionsHelper.SetSuggestionActive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached);
+                    }
+                }
+            } else if (HotReloadSuggestionsHelper.CheckSuggestionActive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached)) {
+                HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached);
+            }
+            
+            if(ServerHealthCheck.I.IsServerHealthy) {
+                // NOTE: avoid calling this method when HR is not running to avoid allocations
+                RequestServerInfo();
+                TryPrepareBuildInfo();
+                if (!requestingCompile && (!config.patchEditModeOnlyOnEditorFocus || Application.isPlaying || UnityEditorInternal.InternalEditorUtility.isApplicationActive)) {
+                    RequestHelper.PollMethodPatches(HotReloadState.LastPatchId, resp => HandleResponseReceived(resp));
+                }
+                RequestHelper.PollPatchStatus(resp => {
+                    patchStatus = resp.patchStatus;
+                    if (patchStatus == PatchStatus.Compiling) {
+                        startWaitingForCompile = null;
+                    }
+                    if (patchStatus == PatchStatus.Patching) {
+                        firstPatchAttempted = true;
+                        if (HotReloadPrefs.ShowPatchingNotifications) {
+                            EditorWindowHelper.ShowNotification(EditorWindowHelper.NotificationStatus.Patching, maxDuration: 10);
+                        }
+                    } else if (HotReloadPrefs.ShowPatchingNotifications) {
+                        EditorWindowHelper.RemoveNotification();
+                    }
+                }, patchStatus);
+                if (HotReloadPrefs.AllAssetChanges) {
+                    RequestHelper.PollAssetChanges(HandleAssetChange);
+                }
+#if UNITY_2020_1_OR_NEWER
+                if (!disableInlineChecks) {
+                    CheckInlinedMethods();
+                }
+#endif
+            }
+            if (!ServerHealthCheck.I.IsServerHealthy) {
+                stopping = false;
+            }
+            if (startupProgress?.Item1 == 1) {
+                starting = false;
+            }
+            if (!_requestingFlushErrors && Running) {
+                RequestFlushErrors().Forget();
+            }
+            CheckEditorSettings();
+        }
+        
+#if UNITY_2020_1_OR_NEWER
+        //only disabled for integration tests
+        internal static bool disableInlineChecks = false;
+        internal static HashSet<MethodBase> inlinedMethodsFound = new HashSet<MethodBase>();
+        internal static void CheckInlinedMethods() {
+            if (CompilationPipeline.codeOptimization != CodeOptimization.Release) {
+                return;
+            }
+            HashSet<MethodBase> newInlinedMethods = null;
+            try {
+                foreach (var method in CodePatcher.I.OriginalPatchMethods) {
+                    if (inlinedMethodsFound.Contains(method)) {
+                        continue;
+                    }
+                    var isMethodSynthesized = method.Name.Contains("<") || method.DeclaringType?.Name.Contains("<") == true && method.Name == ".ctor";
+                    if (!(method is ConstructorInfo) && !isMethodSynthesized && MethodUtils.IsMethodInlined(method)) {
+                        if (newInlinedMethods == null) {
+                            newInlinedMethods = new HashSet<MethodBase>();
+                        }
+                        newInlinedMethods.Add(method);
+                    }
+                }
+                if (newInlinedMethods?.Count > 0) {
+                    if (!HotReloadPrefs.LoggedInlinedMethodsDialogue) {
+                        Log.Warning("Unity Editor inlines simple methods when it's in \"Release\" mode, which Hot Reload cannot patch.\n\nSwitch to Debug mode to avoid this problem, or let Hot Reload fully recompile Unity when this issue occurs.");
+                        HotReloadPrefs.LoggedInlinedMethodsDialogue = true;
+                    }
+                    HotReloadTimelineHelper.CreateInlinedMethodsEntry(entryType: EntryType.Foldout, patchedMethodsDisplayNames: newInlinedMethods.Select(mb => $"{mb.DeclaringType?.Name}::{mb.Name}").ToArray());
+                    if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) {
+                        TryRecompileUnsupportedChanges();
+                    }
+                    HotReloadSuggestionsHelper.SetSuggestionActive(HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods);
+                    foreach (var newInlinedMethod in newInlinedMethods) {
+                        inlinedMethodsFound.Add(newInlinedMethod);
+                    }
+                    RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Patching, StatEventType.Inlined)).Forget();
+                }
+            } catch (Exception e) {
+                Log.Warning($"Inline method checker ran into an exception. Please contact support with the exception message to investigate the problem. Exception: {e.Message}");
+            }
+        }
+#endif
+
+        static void CheckEditorSettings() {
+            if (quitting) {
+                return;
+            }
+            CheckAutoRefresh();
+            CheckScriptCompilation();
+            CheckPlaymodeTint();
+            CheckAssetDatabaseRefresh();
+        }
+
+        static void CheckAutoRefresh() {
+            if (HotReloadPrefs.AllowDisableUnityAutoRefresh && ServerHealthCheck.I.IsServerHealthy) {
+                AutoRefreshSettingChecker.Apply();
+                AutoRefreshSettingChecker.Check();
+            } else {
+                AutoRefreshSettingChecker.Reset();
+            }
+        }
+        
+        static void CheckScriptCompilation() {
+            if (HotReloadPrefs.AllowDisableUnityAutoRefresh && ServerHealthCheck.I.IsServerHealthy) {
+                ScriptCompilationSettingChecker.Apply();
+                ScriptCompilationSettingChecker.Check();
+            } else {
+                ScriptCompilationSettingChecker.Reset();
+            }
+        }
+        
+        static string[] assetExtensionBlacklist = new[] {
+            ".cs",
+            // we can add setting to allow scenes to get hot reloaded for users who collaborate (their scenes change externally)
+            ".unity",
+            // safer to ignore meta files completely until there's a use-case
+            ".meta",
+            // debug files
+            ".mdb",
+            ".pdb",
+            ".compute",
+            // ".shader", //use assetBlacklist instead
+        };
+
+        public static string[] compileFiles = new[] {
+            ".asmdef",
+            ".asmref",
+            ".rsp",
+        };
+
+        public static string[] plugins = new[] {
+            // native plugins
+            ".dll",
+            ".bundle",
+            ".dylib",
+            ".so",
+            // plugin scripts
+            ".cpp",
+            ".h",
+            ".aar",
+            ".jar",
+            ".a",
+            ".java"
+        };
+        
+        static void HandleAssetChange(string assetPath) {
+            // ignore directories
+            if (Directory.Exists(assetPath)) {
+                return;
+            }
+            // ignore temp compile files
+            if (assetPath.Contains("UnityDirMonSyncFile") || assetPath.EndsWith("~", StringComparison.Ordinal)) {
+                return;
+            }
+            foreach (var compileFile in compileFiles) {
+                if (assetPath.EndsWith(compileFile, StringComparison.Ordinal)) {
+                    HotReloadTimelineHelper.CreateErrorEventEntry($"errors: AssemblyFileEdit: Editing assembly files requires recompiling in Unity. in {assetPath}", entryType: EntryType.Foldout);
+                    _applyingFailed = true;
+                    if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) {
+                        TryRecompileUnsupportedChanges();
+                    }
+                    return;
+                }
+            }
+            // Add plugin changes to unsupported changes list
+            foreach (var plugin in plugins) {
+                if (assetPath.EndsWith(plugin, StringComparison.Ordinal)) {
+                    HotReloadTimelineHelper.CreateErrorEventEntry($"errors: NativePluginEdit: Editing native plugins requires recompiling in Unity. in {assetPath}", entryType: EntryType.Foldout);
+                    _applyingFailed = true;
+                    if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) {
+                        TryRecompileUnsupportedChanges();
+                    }
+                    return;
+                }
+            }
+
+            // ignore file extensions that trigger domain reload
+            if (!HotReloadPrefs.IncludeShaderChanges) { 
+                if (assetPath.EndsWith(".shader", StringComparison.Ordinal)) {
+                    return;
+                }
+            }
+            foreach (var blacklisted in assetExtensionBlacklist) {
+                if (assetPath.EndsWith(blacklisted, StringComparison.Ordinal)) {
+                    return;
+                }
+            }
+            if (config?.assetBlacklist != null) {
+                foreach (var blacklisted in config.assetBlacklist) {
+                    if (assetPath.EndsWith(blacklisted, StringComparison.Ordinal)) {
+                        return;
+                    }
+                }
+            }
+            var path = ToPath(assetPath);
+            if (path == null) {
+                return;
+            }
+            try {
+                if (!File.Exists(assetPath)) {
+                    AssetDatabase.DeleteAsset(path);
+                } else {
+                    AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
+                }
+            } catch (Exception e){
+                Log.Warning($"Refreshing asset at path: {assetPath} failed due to exception: {e}");
+            }
+        }
+
+        static string ToPath(string assetPath) {
+            var relativePath = GetRelativePath(assetPath, Path.GetFullPath("Assets"));
+            var relativePathPackages = GetRelativePath(assetPath, Path.GetFullPath("Packages"));
+            // ignore files outside assets and packages folders
+            if (relativePath.StartsWith("..", StringComparison.Ordinal)) {
+                relativePath = null;
+            }
+            if (relativePathPackages.StartsWith("..", StringComparison.Ordinal)) {
+                relativePathPackages = null;
+                #if UNITY_2021_1_OR_NEWER
+                // Might be inside a package "file:"
+                try {
+                    foreach (var package in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages()) {
+                        if (assetPath.StartsWith(package.resolvedPath.Replace("\\", "/"), StringComparison.Ordinal)) {
+                            relativePathPackages = $"Packages/{package.name}/{assetPath.Substring(package.resolvedPath.Length)}";
+                            break;
+                        }
+                    }
+                } catch {
+                    // ignore
+                }
+                #endif
+            }
+            return relativePath ?? relativePathPackages;
+        }
+
+        public static string GetRelativePath(string filespec, string folder) {
+            Uri pathUri = new Uri(filespec);
+            Uri folderUri = new Uri(folder);
+            return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar));
+        }
+        
+        static void CheckPlaymodeTint() {
+            if (config.changePlaymodeTint && ServerHealthCheck.I.IsServerHealthy && Application.isPlaying) {
+                PlaymodeTintSettingChecker.Apply();
+                PlaymodeTintSettingChecker.Check();
+            } else {
+                PlaymodeTintSettingChecker.Reset();
+            }
+        }
+        
+        static void CheckAssetDatabaseRefresh() {
+            if (config.disableCompilingFromEditorScripts && ServerHealthCheck.I.IsServerHealthy) {
+                CompileMethodDetourer.Apply();
+            } else {
+                CompileMethodDetourer.Reset();
+            }
+        }
+
+        static void HandleResponseReceived(MethodPatchResponse response) {
+            RegisterPatchesResult patchResult = null;
+            if (response.patches?.Length > 0 
+                || response.alteredFields.Length > 0 
+                || response.removedFieldInitializers.Length > 0 
+                || response.addedFieldInitializerInitializers.Length > 0
+                || response.addedFieldInitializerFields.Length > 0
+            ) {
+                LogBurstHint(response);
+                patchResult = CodePatcher.I.RegisterPatches(response, persist: true);
+                CodePatcher.I.SaveAppliedPatches(patchesFilePath).Forget();
+            }
+            
+            if (patchResult?.inspectorModified == true) {
+                // repaint all views calls all gui callbacks but doesn't rebuild the visual tree
+                // which is needed to hide removed fields
+                UnityFieldDrawerPatchHelper.repaintVisualTree = true;
+                InternalEditorUtility.RepaintAllViews();
+            }
+
+            var partiallySupportedChangesFiltered = new List<PartiallySupportedChange>(response.partiallySupportedChanges ?? Array.Empty<PartiallySupportedChange>());
+            partiallySupportedChangesFiltered.RemoveAll(x => !HotReloadTimelineHelper.GetPartiallySupportedChangePref(x));
+            if (!HotReloadPrefs.DisplayNewMonobehaviourMethodsAsPartiallySupported && partiallySupportedChangesFiltered.Remove(PartiallySupportedChange.AddMonobehaviourMethod)) {
+                if (HotReloadSuggestionsHelper.CanShowServerSuggestion(HotReloadSuggestionKind.AddMonobehaviourMethod)) {
+                    HotReloadSuggestionsHelper.SetServerSuggestionShown(HotReloadSuggestionKind.AddMonobehaviourMethod);
+                }
+            }
+            var failuresDeduplicated = new HashSet<string>(response.failures ?? Array.Empty<string>());
+
+            foreach (var hotReloadSuggestionKind in response.suggestions) {
+                if (HotReloadSuggestionsHelper.CanShowServerSuggestion(hotReloadSuggestionKind)) {
+                    HotReloadSuggestionsHelper.SetServerSuggestionShown(hotReloadSuggestionKind);
+                }
+            }
+
+            var allMethods = patchResult?.patchedSMethods.Select(m => GetExtendedMethodName(m));
+            if (allMethods == null) {
+                allMethods = response.removedMethod?.Select(m => GetExtendedMethodName(m)).Distinct(StringComparer.OrdinalIgnoreCase) ?? Array.Empty<string>();
+            } else {
+                allMethods = allMethods.Concat(response.removedMethod?.Select(m => GetExtendedMethodName(m)) ?? Array.Empty<string>()).Distinct(StringComparer.OrdinalIgnoreCase);
+            }
+            
+            var allFields = (patchResult?.addedFields.Select(f => GetExtendedFieldName(f)) ?? Array.Empty<string>())
+                            .Concat(response.alteredFields?.Select(f => GetExtendedFieldName(f)).Distinct(StringComparer.OrdinalIgnoreCase) ?? Array.Empty<string>())
+                            .Concat(response.patches?.SelectMany(p => p?.propertyAttributesFieldUpdated ?? Array.Empty<SField>()).Select(f => GetExtendedFieldName(f)).Distinct(StringComparer.OrdinalIgnoreCase) ?? Array.Empty<string>())
+                            .Distinct(StringComparer.OrdinalIgnoreCase);
+            
+            var patchedMembersDisplayNames = allMethods.Concat(allFields).ToArray();
+            
+            _compileError = response.failures?.Any(failure => failure.Contains("error CS")) ?? false;
+            _applyingFailed = response.failures?.Length > 0 || patchResult?.patchFailures.Count > 0 || patchResult?.patchExceptions.Count > 0;
+            _appliedPartially = !_applyingFailed && partiallySupportedChangesFiltered.Count > 0;
+            _appliedUndetected = patchedMembersDisplayNames.Length == 0;
+
+            if (!_compileError) {
+                lastCompileErrorLog = null;
+            }
+
+            var autoRecompiled = false;
+            if (_compileError) {
+                HotReloadTimelineHelper.EventsTimeline.RemoveAll(e => e.alertType == AlertType.CompileError);
+                foreach (var failure in failuresDeduplicated) {
+                    if (failure.Contains("error CS")) {
+                        HotReloadTimelineHelper.CreateErrorEventEntry(failure);
+                    }
+                }
+                if (lastCompileErrorLog != null) {
+                    Log.Error(lastCompileErrorLog);
+                    lastCompileErrorLog = null;
+                }
+                RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Reload, StatEventType.CompileError), new EditorExtraData {
+                    { StatKey.PatchId, response.id },
+                }).Forget();
+            } else if (_applyingFailed) {
+                if (partiallySupportedChangesFiltered.Count > 0) {
+                    foreach (var responsePartiallySupportedChange in partiallySupportedChangesFiltered) {
+                        HotReloadTimelineHelper.CreatePartiallyAppliedEventEntry(responsePartiallySupportedChange, entryType: EntryType.Child);
+                    }
+                }
+                foreach (var failure in failuresDeduplicated) {
+                    HotReloadTimelineHelper.CreateErrorEventEntry(failure, entryType: EntryType.Child);
+                }
+                if (patchResult?.patchFailures.Count > 0) {
+                    foreach (var failure in patchResult.patchFailures) {
+                        SMethod method = failure.Item1;
+                        string error = failure.Item2;
+                        HotReloadTimelineHelper.CreatePatchFailureEventEntry(error, methodName: GetMethodName(method), methodSimpleName: method.simpleName, entryType: EntryType.Child);
+                    }
+                }
+                if (patchResult?.patchExceptions.Count > 0) {
+                    foreach (var error in patchResult.patchExceptions) {
+                        HotReloadTimelineHelper.CreateErrorEventEntry(error, entryType: EntryType.Child);
+                    }
+                }
+                HotReloadTimelineHelper.CreateReloadFinishedWithWarningsEventEntry(patchedMembersDisplayNames: patchedMembersDisplayNames);
+                HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.UnsupportedChanges);
+                if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) {
+                    autoRecompiled = TryRecompileUnsupportedChanges();
+                }
+                RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Reload, StatEventType.Failure), new EditorExtraData {
+                    { StatKey.PatchId, response.id },
+                }).Forget();
+            } else if (_appliedPartially) {
+                foreach (var responsePartiallySupportedChange in partiallySupportedChangesFiltered) {
+                    HotReloadTimelineHelper.CreatePartiallyAppliedEventEntry(responsePartiallySupportedChange, entryType: EntryType.Child, detailed: false);
+                }
+                HotReloadTimelineHelper.CreateReloadPartiallyAppliedEventEntry(patchedMethodsDisplayNames: patchedMembersDisplayNames);
+                
+                if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) {
+                    autoRecompiled = TryRecompileUnsupportedChanges();
+                }
+                RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Reload, StatEventType.Partial), new EditorExtraData {
+                    { StatKey.PatchId, response.id },
+                }).Forget();
+            } else if (_appliedUndetected)  {
+                HotReloadTimelineHelper.CreateReloadUndetectedChangeEventEntry();
+                RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Reload, StatEventType.Undetected), new EditorExtraData {
+                    { StatKey.PatchId, response.id },
+                }).Forget();
+            } else {
+                HotReloadTimelineHelper.CreateReloadFinishedEventEntry(patchedMethodsDisplayNames: patchedMembersDisplayNames);
+                RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Reload, StatEventType.Finished), new EditorExtraData {
+                    { StatKey.PatchId, response.id },
+                }).Forget();
+            }
+
+            // When patching different assembly, compile error will get removed, even though it's still there
+            // It's a shortcut we take for simplicity
+            if (!_compileError) {
+                HotReloadTimelineHelper.EventsTimeline.RemoveAll(x => x.alertType == AlertType.CompileError);
+            }
+
+            foreach (string responseFailure in response.failures) {
+                if (responseFailure.Contains("error CS")) {
+                    Log.Error(responseFailure);
+                } else if (autoRecompiled) {
+                    Log.Info(responseFailure);
+                } else {
+                    Log.Warning(responseFailure);
+                }
+            }
+            if (patchResult?.patchFailures.Count > 0) {
+                foreach (var patchResultPatchFailure in patchResult.patchFailures) {
+                    if (autoRecompiled) {
+                        Log.Info(patchResultPatchFailure.Item2);
+                    } else {
+                        Log.Warning(patchResultPatchFailure.Item2);
+                    }
+                }
+            }
+            if (patchResult?.patchExceptions.Count > 0) {
+                foreach (var patchResultPatchException in patchResult.patchExceptions) {
+                    if (autoRecompiled) {
+                        Log.Info(patchResultPatchException);
+                    } else {
+                        Log.Warning(patchResultPatchException);
+                    }
+                }
+            }
+            
+            // attempt to recompile if previous Unity compilation had compilation errors
+            // because new changes might've fixed those errors
+            if (compileChecker.hasCompileErrors) {
+                HotReloadRunTab.Recompile();
+            }
+
+            if (HotReloadWindow.Current) {
+                HotReloadWindow.Current.Repaint();
+            }
+            HotReloadState.LastPatchId = response.id;
+            OnPatchHandled?.Invoke((response, patchResult));
+        }
+        
+        static string GetExtendedMethodName(SMethod method) {
+            var colonIndex = method.displayName.IndexOf("::", StringComparison.Ordinal);
+            if (colonIndex > 0) {
+                var beforeColon = method.displayName.Substring(0, colonIndex);
+                var spaceIndex = beforeColon.LastIndexOf(".", StringComparison.Ordinal);
+                if (spaceIndex > 0) {
+                    var className = beforeColon.Substring(spaceIndex + 1);
+                    return className + "::" + method.simpleName;
+                }
+            }
+            return method.simpleName;
+        }
+        
+        static string GetExtendedFieldName(SField field) {
+            string typeName = field.declaringType.typeName;
+            var simpleTypeIndex = typeName.LastIndexOf(".", StringComparison.Ordinal);
+            if (simpleTypeIndex > 0) {
+                typeName = typeName.Substring(simpleTypeIndex + 1);
+            }
+            return $"{typeName}::{field.fieldName}";
+        }
+
+        static string GetMethodName(SMethod method) {
+            var spaceIndex = method.displayName.IndexOf(" ", StringComparison.Ordinal);
+            if (spaceIndex > 0) {
+                return method.displayName.Substring(spaceIndex);
+            }
+            return method.displayName;
+        }
+
+        
+        [Conditional("UNITY_2022_2_OR_NEWER")]
+        static void LogBurstHint(MethodPatchResponse response) {
+            if(HotReloadPrefs.LoggedBurstHint) {
+                return;
+            }
+            foreach (var patch in response.patches) {
+                if(patch.unityJobs.Length > 0) {
+                    Debug.LogWarning("A unity job was hot reloaded. " +
+                                     "This will cause a harmless warning that can be ignored. " +
+                                     $"More info about this can be found here: {Constants.TroubleshootingURL}");
+                    HotReloadPrefs.LoggedBurstHint = true;
+                    break;
+                }
+            }
+        }
+
+        private static DateTime? startWaitingForCompile;
+        static void OnCompilationFinished() {
+            ServerHealthCheck.instance.CheckHealth();
+            if(ServerHealthCheck.I.IsServerHealthy) {
+                startWaitingForCompile = DateTime.UtcNow;
+                firstPatchAttempted = false;
+                RequestCompile().Forget();
+            }
+            ClearPersistence();
+        }
+        
+        static void ClearPersistence() {
+            Task.Run(() => File.Delete(patchesFilePath));
+            HotReloadTimelineHelper.ClearPersistance();
+        }
+
+        static bool requestingCompile;
+        static async Task RequestCompile() {
+            requestingCompile = true;
+            try {
+                await RequestHelper.RequestClearPatches();
+                await ProjectGeneration.ProjectGeneration.GenerateSlnAndCsprojFiles(Application.dataPath);
+                await RequestHelper.RequestCompile(scenePath => {
+                    var path = ToPath(scenePath);
+                    if (File.Exists(scenePath) && path != null) {
+                        AssetDatabase.ImportAsset(path, ImportAssetOptions.Default);
+                    }
+                });
+            } finally {
+                requestingCompile = false;
+            }
+        }
+        
+        private static bool stopping;
+        private static bool starting;
+        private static DateTime? startupCompletedAt;
+        private static Tuple<float, string> startupProgress;
+        
+        internal static bool Started => ServerHealthCheck.I.IsServerHealthy && DownloadProgress == 1 && StartupProgress?.Item1 == 1;
+        internal static bool Starting => (StartedServerRecently() || ServerHealthCheck.I.IsServerHealthy) && !Started && starting && patchStatus != PatchStatus.CompileError;
+        internal static bool Stopping => stopping && Running;
+        internal static bool Compiling => DateTime.UtcNow - startWaitingForCompile < TimeSpan.FromSeconds(5) || patchStatus == PatchStatus.Compiling || HotReloadRunTab.recompiling;
+        internal static Tuple<float, string> StartupProgress => startupProgress;
+        
+        
+        /// <summary>
+        /// We have a button to stop the Hot Reload server.<br/>
+        /// Store task to ensure only one stop attempt at a time. 
+        /// </summary>
+        private static DateTime? serverStartedAt;
+        private static DateTime? serverStoppedAt;
+        private static DateTime? serverRestartedAt;
+        private static bool StartedServerRecently() {
+            return DateTime.UtcNow - serverStartedAt < ServerHealthCheck.HeartBeatTimeout;
+        }
+        
+        internal static bool StoppedServerRecently() {
+            return DateTime.UtcNow - serverStoppedAt < ServerHealthCheck.HeartBeatTimeout || (!StartedServerRecently() && (startupProgress?.Item1 ?? 0) == 0);
+        }
+        
+        internal static bool RestartedServerRecently() {
+            return DateTime.UtcNow - serverRestartedAt < ServerHealthCheck.HeartBeatTimeout;
+        }
+
+        private static bool requestingStart;
+        private static async Task StartCodePatcher(LoginData loginData = null) {
+            if (requestingStart || StartedServerRecently())  {
+                return;
+            }
+            stopping = false;
+            starting = true;
+            var exposeToNetwork = HotReloadPrefs.ExposeServerToLocalNetwork;
+            var allAssetChanges = HotReloadPrefs.AllAssetChanges;
+            var disableConsoleWindow = HotReloadPrefs.DisableConsoleWindow;
+            var isReleaseMode = RequestHelper.IsReleaseMode();
+            var detailedErrorReporting = !HotReloadPrefs.DisableDetailedErrorReporting;
+            CodePatcher.I.ClearPatchedMethods();
+            RecordActiveDaysForRateApp();
+            try {
+                requestingStart = true;
+                startupProgress = Tuple.Create(0f, "Starting Hot Reload");
+                serverStartedAt = DateTime.UtcNow;
+                await HotReloadCli.StartAsync(exposeToNetwork, allAssetChanges, disableConsoleWindow, isReleaseMode, detailedErrorReporting, loginData).ConfigureAwait(false);
+            }
+            catch (Exception ex) {
+                ThreadUtility.LogException(ex);
+            }
+            finally {
+                requestingStart = false;
+            }
+        }
+        
+        private static bool requestingStop;
+        internal static async Task StopCodePatcher(bool recompileOnDone = false) {
+            stopping = true;
+            starting = false;
+            if (requestingStop) {
+                if (recompileOnDone) {
+                    await ThreadUtility.SwitchToMainThread();
+                    HotReloadRunTab.Recompile();
+                }
+                return;
+            }
+            CodePatcher.I.ClearPatchedMethods();
+            HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
+            try {
+                requestingStop = true;
+                await HotReloadCli.StopAsync().ConfigureAwait(false);
+                serverStoppedAt = DateTime.UtcNow;
+                await ThreadUtility.SwitchToMainThread();
+                if (recompileOnDone) {
+                    HotReloadRunTab.Recompile();
+                }
+                startupProgress = null;
+            }
+            catch (Exception ex) {
+                ThreadUtility.LogException(ex);
+            }
+            finally {
+                requestingStop = false;
+            }
+        }
+        
+        private static bool requestingRestart;
+        internal static async Task RestartCodePatcher() {
+            if (requestingRestart) {
+                return;
+            }
+            try {
+                requestingRestart = true;
+                await StopCodePatcher();
+                await DownloadAndRun();
+                serverRestartedAt = DateTime.UtcNow;
+            }
+            finally {
+                requestingRestart = false;
+            }
+        }
+        
+        
+        private static bool requestingDownloadAndRun;
+        internal static float DownloadProgress => serverDownloader.Progress;
+        internal static bool DownloadRequired => DownloadProgress < 1f;
+        internal static bool DownloadStarted => serverDownloader.Started;
+        internal static bool RequestingDownloadAndRun => requestingDownloadAndRun;
+        internal static async Task<bool> DownloadAndRun(LoginData loginData = null, bool recompileOnDone = false) {
+            if (requestingDownloadAndRun) {
+                return false;
+            }
+            stopping = false;
+            requestingDownloadAndRun = true;
+            try {
+                if (DownloadRequired) {
+                    var ok = await serverDownloader.PromptForDownload();
+                    if (!ok) {
+                        return false;
+                    }
+                }
+                await StartCodePatcher(loginData);
+                await ThreadUtility.SwitchToMainThread();
+                if (HotReloadPrefs.DeactivateHotReload) {
+                    HotReloadPrefs.DeactivateHotReload = false;
+                    HotReloadRunTab.Recompile();
+                }
+                return true;
+            } finally {
+                requestingDownloadAndRun = false;
+            }
+        }
+        
+        private const int SERVER_POLL_FREQUENCY_ON_STARTUP_MS = 500;
+        private const int SERVER_POLL_FREQUENCY_AFTER_STARTUP_MS = 2000;
+        private static int GetPollFrequency() {
+            return (startupProgress != null && startupProgress.Item1 < 1) || StartedServerRecently()
+                ? SERVER_POLL_FREQUENCY_ON_STARTUP_MS
+                : SERVER_POLL_FREQUENCY_AFTER_STARTUP_MS;
+        }
+        
+        internal static bool RequestingLoginInfo { get; set; }
+        
+        [CanBeNull] internal static LoginStatusResponse Status { get; private set; }
+        internal static void HandleStatus(LoginStatusResponse resp) {
+            if (resp == null) {
+                return;
+            }
+            Attribution.RegisterLogin(resp);
+            
+            bool consumptionsChanged = Status?.freeSessionRunning != resp.freeSessionRunning || Status?.freeSessionEndTime != resp.freeSessionEndTime;
+            bool expiresAtChanged = Status?.licenseExpiresAt != resp.licenseExpiresAt;
+            if (!EditorCodePatcher.LoginNotRequired 
+                && resp.consumptionsUnavailableReason == ConsumptionsUnavailableReason.UnrecoverableError
+                && Status?.consumptionsUnavailableReason != ConsumptionsUnavailableReason.UnrecoverableError
+            ) {
+                Log.Error("Free charges unavailabe. Please contact support if the issue persists.");
+            }
+            if (!RequestingLoginInfo && resp.requestError == null) {
+                Status = resp;
+            }
+            if (resp.lastLicenseError == null) {
+                // If we got success, we should always show an error next time it comes up
+                HotReloadPrefs.ErrorHidden = false;
+            }
+
+            var oldStartupProgress = startupProgress;
+            var newStartupProgress = Tuple.Create(
+                resp.startupProgress,
+                string.IsNullOrEmpty(resp.startupStatus) ? "Starting Hot Reload" : resp.startupStatus);
+
+            startupProgress = newStartupProgress;
+            // ReSharper disable once CompareOfFloatsByEqualityOperator
+            if (startupCompletedAt == null && newStartupProgress.Item1 == 1f) {
+                startupCompletedAt = DateTime.UtcNow;
+            }
+            
+            if (oldStartupProgress == null
+                || Math.Abs(oldStartupProgress.Item1 - newStartupProgress.Item1) > 0
+                || oldStartupProgress.Item2 != newStartupProgress.Item2
+                || consumptionsChanged
+                || expiresAtChanged
+            ) {
+                // Send project files state now that server can receive requests (only needed for player builds)
+                TryPrepareBuildInfo();
+            }
+        }
+        
+        internal static  async Task RequestLogin(string email, string password) {
+            RequestingLoginInfo = true;
+            try {
+                int i = 0;
+                while (!Running && i < 100) {
+                    await Task.Delay(100);
+                    i++;
+                }
+
+                Status = await RequestHelper.RequestLogin(email, password, 10);
+
+                // set to false so new error is shown
+                HotReloadPrefs.ErrorHidden = false;
+                if (Status?.isLicensed == true) {
+                    HotReloadPrefs.LicenseEmail = email;
+                    HotReloadPrefs.LicensePassword = Status.initialPassword ?? password;
+                }
+            } finally {
+                RequestingLoginInfo = false;
+            }
+        }
+        private static bool requestingServerInfo;
+        private static long lastServerPoll;
+        private static bool running;
+        internal static bool Running => ServerHealthCheck.I.IsServerHealthy;
+        
+        internal static void RequestServerInfo() {
+            if (requestingServerInfo) {
+                return;
+            }
+            RequestServerInfoAsync().Forget();
+        }
+        
+        private static async Task RequestServerInfoAsync() {
+            requestingServerInfo = true;
+            try {
+                await RequestServerInfoCore();
+            } finally {
+                requestingServerInfo = false;
+            }
+        }
+
+        private static async Task RequestServerInfoCore() {
+            var pollFrequency = GetPollFrequency();
+            // Delay until we've hit the poll request frequency
+            var waitMs = (int)Mathf.Clamp(pollFrequency - ((DateTime.Now.Ticks / (float)TimeSpan.TicksPerMillisecond) - lastServerPoll), 0, pollFrequency);
+            await Task.Delay(waitMs);
+
+            if (!ServerHealthCheck.I.IsServerHealthy) {
+                return;
+            }
+
+            
+            var resp = await RequestHelper.GetLoginStatus(30);
+            HandleStatus(resp);
+
+            lastServerPoll = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
+        }
+    }
+    
+    // IMPORTANT: don't change the names of the methods
+    internal static class UnityFieldDrawerPatchHelper {
+        internal static void PatchCustom(Rect contentRect, UnityEditor.Editor __instance) {
+            if (__instance.target) {
+                FieldDrawerUtil.DrawFromObject(__instance.target);
+            }
+        }
+
+        internal static void PatchDefault(UnityEditor.Editor __instance) {
+            if (__instance.target) {
+                FieldDrawerUtil.DrawFromObject(__instance.target);
+            }
+        }
+
+        internal static bool repaintVisualTree;
+        internal static void PatchFillDefaultInspector(VisualElement container, SerializedObject serializedObject, UnityEditor.Editor editor) {
+            HideChildren(container, serializedObject);
+            if (editor.target) {
+                var child = new IMGUIContainer((() =>
+                {
+                    FieldDrawerUtil.DrawFromObject(editor.target);
+                    if (repaintVisualTree) {
+                        HideChildren(container, serializedObject);
+                        ResetInvalidatedInspectorFields(container, serializedObject);
+                        // Mark dirty to repaint the visual tree
+                        container.MarkDirtyRepaint();
+                        repaintVisualTree = false;
+                    }
+                }));
+                child.name = "SingularityGroup.HotReload.FieldDrawer";
+                container.Add(child);
+            }
+        }
+
+        static List<VisualElement> childrenToRemove = new List<VisualElement>();
+        static void HideChildren(VisualElement container, SerializedObject serializedObject) {
+            if (container == null) {
+                return;
+            } 
+            childrenToRemove.Clear();
+            foreach (var child in container.Children()) {
+                if (!(child is PropertyField propertyField)) {
+                    continue;
+                }
+                try {
+                    if (serializedObject != null && serializedObject.targetObject && UnityFieldHelper.IsFieldHidden(serializedObject.targetObject.GetType(), serializedObject.FindProperty(propertyField.bindingPath)?.name ?? "")) {
+                        childrenToRemove.Add(child);
+                    }
+                } catch (NullReferenceException) {
+                    // serializedObject.targetObject throws nullref in cases where e.g. exising playmode
+                }
+            }
+            foreach (var child in childrenToRemove) {
+                container.Remove(child);
+            }
+            childrenToRemove.Clear();
+        }
+        
+        static void ResetInvalidatedInspectorFields(VisualElement container, SerializedObject serializedObject) {
+            if (container == null || serializedObject == null) {
+                return;
+            } 
+            foreach (var child in container.Children()) {
+                if (!(child is PropertyField propertyField)) {
+                    continue;
+                }
+                try {
+                    var prop = serializedObject.FindProperty(propertyField.bindingPath);
+                    if (prop != null && serializedObject.targetObject && UnityFieldHelper.HasFieldInspectorCacheInvalidation(serializedObject.targetObject.GetType(), prop.name ?? "")) {
+                        child.GetType().GetMethod("Reset", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(SerializedProperty) }, null)?.Invoke(child, new object[] { prop });
+                    }
+                } catch (NullReferenceException) {
+                    // serializedObject.targetObject throws nullref in cases where e.g. exising playmode
+                }
+            }
+        }
+        
+        internal static bool GetHandlerPrefix(
+            SerializedProperty property,
+            ref object __result
+        ) {
+            if (property == null || property.serializedObject == null || !property.serializedObject.targetObject) {
+                // do nothing
+                return true;
+            }
+            if (UnityFieldHelper.TryInvalidateFieldInspectorCache(property.serializedObject.targetObject.GetType(), property.name)) {
+                __result = null;
+                return false;
+            }
+            return true;
+        }
+
+        internal static bool GetFieldAttributesPrefix(
+            FieldInfo field,
+            ref List<PropertyAttribute> __result
+        ) {
+            if (field == null) {
+                // do nothing
+                return true;
+            }
+            List<PropertyAttribute> result;
+            if (UnityFieldHelper.TryGetInspectorFieldAttributes(field, out result)) {
+                __result = result;
+                return false;
+            }
+            return true;
+        }
+
+        internal static bool PropertyFieldPrefix(
+            Rect position,
+            UnityEditor.SerializedProperty property,
+            GUIContent label,
+            bool includeChildren,
+            Rect visibleArea,
+            ref bool __result
+        ) {
+            if (property == null || property.serializedObject == null || !property.serializedObject.targetObject) {
+                // do nothing
+                return true;
+            }
+            if (UnityFieldHelper.IsFieldHidden(property.serializedObject.targetObject.GetType(), property.name)) {
+                // make sure field doesn't take any space
+                __result = false;
+                return false; // Skip original method
+            }
+            return true; // Continue with original method
+        }
+
+        internal static bool GetHightPrefix(
+            UnityEditor.SerializedProperty property, GUIContent label, bool includeChildren,
+            ref float __result
+        ) {
+            if (property == null || property.serializedObject == null || !property.serializedObject.targetObject) {
+                // do nothing
+                return true;
+            }
+            if (UnityFieldHelper.IsFieldHidden(property.serializedObject.targetObject.GetType(), property.name)) {
+                // make sure field doesn't take any space
+                __result = 0.0f;
+                return false; // Skip original method
+            }
+            return true; // Continue with original method
+        }
+    }
+}

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/EditorCodePatcher.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ac7b192276a4a9d4f9098377d317cb2e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 183 - 0
Packages/com.singularitygroup.hotreload/Editor/EditorIndicationState.cs

@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using SingularityGroup.HotReload.DTO;
+
+namespace SingularityGroup.HotReload.Editor {
+    internal static class EditorIndicationState {
+        internal enum IndicationStatus {  
+            Stopped,
+            Started,
+            Stopping,
+            Installing,
+            Starting,
+            Reloaded,
+            PartiallySupported,
+            Unsupported,
+            Patching,
+            Loading,
+            Compiling,
+            CompileErrors,
+            ActivationFailed,
+            FinishRegistration,
+            Undetected,
+        }
+
+        internal static readonly string greyIconPath = "grey";
+        internal static readonly string greenIconPath = "green";
+        internal static readonly string redIconPath = "red";
+        private static readonly Dictionary<IndicationStatus, string> IndicationIcon = new Dictionary<IndicationStatus, string> {
+            // grey icon:
+            { IndicationStatus.FinishRegistration, greyIconPath },
+            { IndicationStatus.Stopped, greyIconPath },
+            // green icon:
+            { IndicationStatus.Started, greenIconPath },
+            // log icons:
+            { IndicationStatus.Reloaded, HotReloadTimelineHelper.alertIconString[AlertType.AppliedChange] },
+            { IndicationStatus.Unsupported, HotReloadTimelineHelper.alertIconString[AlertType.UnsupportedChange] },
+            { IndicationStatus.Undetected, HotReloadTimelineHelper.alertIconString[AlertType.UndetectedChange] },
+            { IndicationStatus.PartiallySupported, HotReloadTimelineHelper.alertIconString[AlertType.PartiallySupportedChange] },
+            { IndicationStatus.CompileErrors, HotReloadTimelineHelper.alertIconString[AlertType.CompileError] },
+            // spinner:
+            { IndicationStatus.Stopping, Spinner.SpinnerIconPath },
+            { IndicationStatus.Starting, Spinner.SpinnerIconPath },
+            { IndicationStatus.Patching, Spinner.SpinnerIconPath },
+            { IndicationStatus.Loading, Spinner.SpinnerIconPath },
+            { IndicationStatus.Compiling, Spinner.SpinnerIconPath },
+            { IndicationStatus.Installing, Spinner.SpinnerIconPath },
+            // red icon:
+            { IndicationStatus.ActivationFailed, redIconPath },
+        };
+        
+        private static readonly IndicationStatus[] SpinnerIndications = IndicationIcon
+            .Where(kvp => kvp.Value == Spinner.SpinnerIconPath)
+            .Select(kvp => kvp.Key)
+            .ToArray();
+        
+        // NOTE: if you add longer text, make sure UI is wide enough for it
+        public static readonly Dictionary<IndicationStatus, string> IndicationText = new Dictionary<IndicationStatus, string> {
+            { IndicationStatus.FinishRegistration, "Finish Registration" },
+            { IndicationStatus.Started, "Waiting for code changes" },
+            { IndicationStatus.Stopping, "Stopping Hot Reload" },
+            { IndicationStatus.Stopped, "Hot Reload inactive" },
+            { IndicationStatus.Installing, "Installing" },
+            { IndicationStatus.Starting, "Starting Hot Reload" },
+            { IndicationStatus.Reloaded, "Reload finished" },
+            { IndicationStatus.PartiallySupported, "Changes partially applied" },
+            { IndicationStatus.Unsupported, "Finished with warnings" },
+            { IndicationStatus.Patching, "Reloading" },
+            { IndicationStatus.Compiling, "Compiling" },
+            { IndicationStatus.CompileErrors, "Scripts have compile errors" },
+            { IndicationStatus.ActivationFailed, "Activation failed" },
+            { IndicationStatus.Loading, "Loading" },
+            { IndicationStatus.Undetected, "No changes applied"},
+        };
+
+        private const int MinSpinnerDuration = 200;
+        private static DateTime spinnerStartedAt;
+        private static IndicationStatus latestStatus;
+        private static bool SpinnerCompletedMinDuration => DateTime.UtcNow - spinnerStartedAt > TimeSpan.FromMilliseconds(MinSpinnerDuration);
+        private static IndicationStatus GetIndicationStatus() {
+            var status = GetIndicationStatusCore();
+            
+            // Note: performance sensitive code, don't use Link
+            bool newStatusIsSpinner = false;
+            for (var i = 0; i < SpinnerIndications.Length; i++) {
+                if (SpinnerIndications[i] == status) {
+                    newStatusIsSpinner = true;
+                }
+            }
+            bool latestStatusIsSpinner = false;
+            for (var i = 0; i < SpinnerIndications.Length; i++) {
+                if (SpinnerIndications[i] == latestStatus) {
+                    newStatusIsSpinner = true;
+                }
+            }
+            
+            if (status == latestStatus) {
+                return status;
+            } else if (latestStatusIsSpinner) {
+                if (newStatusIsSpinner) {
+                    return status;
+                } else if (SpinnerCompletedMinDuration) {
+                    latestStatus = status;
+                    return status;
+                } else {
+                    return latestStatus;
+                }
+            } else if (newStatusIsSpinner) {
+                spinnerStartedAt = DateTime.UtcNow;
+                latestStatus = status;
+                return status;    
+            } else {
+                spinnerStartedAt = DateTime.UtcNow;
+                latestStatus = IndicationStatus.Loading;
+                return status;
+            }
+        }
+        
+        private static IndicationStatus GetIndicationStatusCore() {
+            if (RedeemLicenseHelper.I.RegistrationRequired)
+                return IndicationStatus.FinishRegistration;
+            if (EditorCodePatcher.DownloadRequired && EditorCodePatcher.DownloadStarted || EditorCodePatcher.RequestingDownloadAndRun && !EditorCodePatcher.Starting && !EditorCodePatcher.Stopping)
+                return IndicationStatus.Installing;
+            if (EditorCodePatcher.Stopping)
+                return IndicationStatus.Stopping;
+            if (EditorCodePatcher.Compiling && !EditorCodePatcher.Stopping && !EditorCodePatcher.Starting && EditorCodePatcher.Running)
+                return IndicationStatus.Compiling;
+            if (EditorCodePatcher.Starting && !EditorCodePatcher.Stopping)
+                return IndicationStatus.Starting;
+            if (!EditorCodePatcher.Running)
+                return IndicationStatus.Stopped;
+            if (EditorCodePatcher.Status?.isLicensed != true && EditorCodePatcher.Status?.isFree != true && EditorCodePatcher.Status?.freeSessionFinished == true)
+                return IndicationStatus.ActivationFailed;
+            if (EditorCodePatcher.compileError)
+                return IndicationStatus.CompileErrors;
+
+            // fallback on patch status
+            if (!EditorCodePatcher.Started && !EditorCodePatcher.Running) {
+                return IndicationStatus.Stopped;
+            }
+            switch (EditorCodePatcher.patchStatus) {
+                case PatchStatus.Idle:
+                    if (!EditorCodePatcher.Compiling && !EditorCodePatcher.firstPatchAttempted && !EditorCodePatcher.compileError) {
+                        return IndicationStatus.Started;
+                    }
+                    if (EditorCodePatcher._applyingFailed) {
+                        return IndicationStatus.Unsupported;
+                    }
+                    if (EditorCodePatcher._appliedPartially) {
+                        return IndicationStatus.PartiallySupported;
+                    }
+                    if (EditorCodePatcher._appliedUndetected) {
+                        return IndicationStatus.Undetected;
+                    }
+                    return IndicationStatus.Reloaded;
+                case PatchStatus.Patching:     return IndicationStatus.Patching;
+                case PatchStatus.Unsupported:  return IndicationStatus.Unsupported;
+                case PatchStatus.Compiling:    return IndicationStatus.Compiling;
+                case PatchStatus.CompileError: return IndicationStatus.CompileErrors;
+                case PatchStatus.None:
+                default:                       return IndicationStatus.Reloaded;
+            }
+        }
+
+        internal static IndicationStatus CurrentIndicationStatus => GetIndicationStatus();
+        internal static bool SpinnerActive => SpinnerIndications.Contains(CurrentIndicationStatus);
+        internal static string IndicationIconPath => IndicationIcon[CurrentIndicationStatus];
+        internal static string IndicationStatusText {
+            get {
+                var indicationStatus = CurrentIndicationStatus;
+                string txt;
+                if (indicationStatus == IndicationStatus.Starting && EditorCodePatcher.StartupProgress != null) {
+                    txt = EditorCodePatcher.StartupProgress.Item2;
+                } else if (!IndicationText.TryGetValue(indicationStatus, out txt)) {
+                    Log.Warning($"Indication text not found for status {indicationStatus}");
+                } else {
+                    txt = IndicationText[indicationStatus];
+                }
+                return txt;
+            }
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/EditorIndicationState.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: ee342ddb17e444c7a8927be3bd792ae2
+timeCreated: 1686087206

+ 87 - 0
Packages/com.singularitygroup.hotreload/Editor/GitUtil.cs

@@ -0,0 +1,87 @@
+
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using Debug = UnityEngine.Debug;
+
+namespace SingularityGroup.HotReload.Editor {
+    internal static class GitUtil {
+        /// <remarks>
+        /// Fallback is PatchServerInfo.UnknownCommitHash
+        /// </remarks>
+        public static string GetShortCommitHashOrFallback(int timeoutAfterMillis = 5000) {
+            var shortCommitHash = PatchServerInfo.UnknownCommitHash;
+            
+            var commitHash = GetShortCommitHashSafe(timeoutAfterMillis);
+            // On MacOS GetShortCommitHash() returns 7 characters, on Windows it returns 8 characters.
+            // When git command produced an unexpected result, use a fallback string
+            if (commitHash != null && commitHash.Length >= 6) {
+                shortCommitHash = commitHash.Length < 8 ? commitHash : commitHash.Substring(0, 8);
+            }
+
+            return shortCommitHash;
+        }
+        
+        // only log exception once per domain reload, to prevent spamming the console
+        private static bool loggedExceptionInGetShortCommitHashSafe = false;
+
+        /// <summary>
+        /// Get the git commit hash, returning null if it takes too long.
+        /// </summary>
+        /// <param name="timeoutAfterMillis"></param>
+        /// <returns></returns>
+        /// <remarks>
+        /// This method is 'better safe than sorry' because we must not break the user's build.<br/>
+        /// It is better to not know the commit hash than to fail the build.
+        /// </remarks>
+        private static string GetShortCommitHashSafe(int timeoutAfterMillis) {
+            Process process = null;
+            // Note: don't use ReadToEndAsync because waiting on that task blocks forever.
+            try {
+                process = StartGitCommand("log", " -n 1 --pretty=format:%h");
+                var stdout = process.StandardOutput;
+                if (process.WaitForExit(timeoutAfterMillis)) {
+                    return stdout.ReadToEnd();
+                } else {
+                    // In a git repo with git lfs, git log can be blocked by waiting for switch branches / download lfs objects
+                    // For that reason I disabled this warning log until a better solution is implemented (e.g. cache the commit and use cached if timeout).
+                    // Log.Warning(
+                    //     $"[{CodePatcher.TAG}] Timed out trying to get the git commit hash, HotReload will not warn you about" +
+                    //     " a build connecting to a server running on a different commit (which is not supported)");
+                    return null;
+                }
+            } catch (Win32Exception ex) {
+                if (ex.NativeErrorCode == 2) {
+                    // git not found, ignore because user doesn't use git for version control
+                    return null;
+                } else if (!loggedExceptionInGetShortCommitHashSafe) {
+                    loggedExceptionInGetShortCommitHashSafe = true;
+                    Debug.LogException(ex);
+                } 
+            } catch (Exception ex) {
+                if (!loggedExceptionInGetShortCommitHashSafe) {
+                    loggedExceptionInGetShortCommitHashSafe = true;
+                    Log.Exception(ex);
+                }
+            } finally {
+                if (process != null) {
+                    process.Dispose();
+                }
+            }
+            return null;
+        }
+
+        static Process StartGitCommand(string command, string arguments, Action<ProcessStartInfo> modifySettings = null) {
+            var startInfo = new ProcessStartInfo("git", command + " " + arguments) {
+                RedirectStandardOutput = true,
+                RedirectStandardError = true,
+                UseShellExecute = false,
+                CreateNoWindow = true,
+            };
+            if (modifySettings != null) {
+                modifySettings(startInfo);
+            }
+            return Process.Start(startInfo);
+        }
+    }
+}

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/GitUtil.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f994bd5bb9f33f740ae37f8c79048a10
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 387b31d7da35b27428629a83bb4ac589
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 188 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/AssemblyOmission.cs

@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using UnityEditor;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using SingularityGroup.HotReload.Newtonsoft.Json;
+using UnityEditor.Compilation;
+
+[assembly: InternalsVisibleTo("SingularityGroup.HotReload.EditorTests")]
+
+namespace SingularityGroup.HotReload.Editor {
+    internal static class AssemblyOmission {
+        // [MenuItem("Window/Hot Reload Dev/List omitted projects")]
+        private static void Check() {
+            Log.Info("To compile C# files same as a Player build, we must omit projects which aren't part of the selected Player build.");
+            var omitted = GetOmittedProjects(EditorUserBuildSettings.activeScriptCompilationDefines);
+            Log.Info("---------");
+
+            foreach (var name in omitted) {
+                Log.Info("omitted editor/other project named: {0}", name);
+            }
+        }
+        
+        [JsonObject(MemberSerialization.Fields)]
+        private class AssemblyDefinitionJson {
+            public string name;
+            public string[] defineConstraints;
+        }
+        
+        // scripts in Assets/ (with no asmdef) are always compiled into Assembly-CSharp
+        private static readonly string alwaysIncluded = "Assembly-CSharp";
+
+        private class Cache : AssetPostprocessor {
+            public static string[] ommitedProjects;
+            
+            private static void OnPostprocessAllAssets(string[] importedAssets,
+                string[] deletedAssets,
+                string[] movedAssets,
+                string[] movedFromAssetPaths) {
+                ommitedProjects = null;
+            }
+        }
+        
+        // main thread only
+        public static string[] GetOmittedProjects(string allDefineSymbols, bool verboseLogs = false) {
+            if (Cache.ommitedProjects != null) {
+                return Cache.ommitedProjects;
+            }
+            var arr = allDefineSymbols.Split(';');
+            var omitted = GetOmittedProjects(arr, verboseLogs);
+            Cache.ommitedProjects = omitted;
+            return omitted;
+        }
+
+        // must be deterministic (return projects in same order each time)
+        private static string[] GetOmittedProjects(string[] allDefineSymbols, bool verboseLogs = false) {
+            // HotReload uses names of assemblies.
+            var editorAssemblies = GetEditorAssemblies();
+
+            editorAssemblies.Remove(alwaysIncluded);
+            var omittedByConstraint = DefineConstraints.GetOmittedAssemblies(allDefineSymbols);
+            editorAssemblies.AddRange(omittedByConstraint);
+
+            // Note: other platform player assemblies are also returned here, but I haven't seen it cause issues
+            //   when using Hot Reload with IdleGame Android build. 
+            var playerAssemblies = GetPlayerAssemblies().ToArray();
+
+            if (verboseLogs) {
+                foreach (var name in editorAssemblies) {
+                    Log.Info("found project named {0}", name);
+                }
+                foreach (var playerAssemblyName in playerAssemblies) {
+                    Log.Debug("player assembly named {0}", playerAssemblyName);
+                }
+            }
+            // leaves the editor assemblies that are not built into player assemblies (e.g. editor and test assemblies)
+            var toOmit = editorAssemblies.Except(playerAssemblies.Select(asm => asm.name));
+            var unique = new HashSet<string>(toOmit);
+            return unique.OrderBy(s => s).ToArray();
+        }
+
+        // main thread only
+        public static List<string> GetEditorAssemblies() {
+            return CompilationPipeline
+                .GetAssemblies(AssembliesType.Editor)
+                .Select(asm => asm.name)
+                .ToList();
+        }
+
+        public static Assembly[] GetPlayerAssemblies() {
+            var playerAssemblyNames = CompilationPipeline
+                #if UNITY_2019_3_OR_NEWER
+                .GetAssemblies(AssembliesType.PlayerWithoutTestAssemblies) // since Unity 2019.3
+                #else
+                .GetAssemblies(AssembliesType.Player)
+                #endif
+                .ToArray();
+            
+
+            return playerAssemblyNames;
+        }
+        
+        internal static class DefineConstraints {
+            /// <summary>
+            /// When define constraints evaluate to false, we need 
+            /// </summary>
+            /// <param name="defineSymbols"></param>
+            /// <returns></returns>
+            /// <remarks>
+            /// Not aware of a Unity api to read defineConstraints, so we do it ourselves.<br/>
+            /// Find any asmdef files where the define constraints evaluate to false.
+            /// </remarks>
+            public static string[] GetOmittedAssemblies(string[] defineSymbols) {
+                var guids = AssetDatabase.FindAssets("t:asmdef");
+                var asmdefFiles = guids.Select(AssetDatabase.GUIDToAssetPath);
+                var shouldOmit = new List<string>();
+                foreach (var asmdefFile in asmdefFiles) {
+                    var asmdef = ReadDefineConstraints(asmdefFile);
+                    if (asmdef == null) continue;
+                    if (asmdef.defineConstraints == null || asmdef.defineConstraints.Length == 0) {
+                        // Hot Reload already handles assemblies correctly if they have no define symbols.
+                        continue;
+                    }
+
+                    var allPass = asmdef.defineConstraints.All(constraint => EvaluateDefineConstraint(constraint, defineSymbols));
+                    if (!allPass) {
+                        shouldOmit.Add(asmdef.name);
+                    }
+                }
+
+                return shouldOmit.ToArray();
+            }
+
+            static AssemblyDefinitionJson ReadDefineConstraints(string path) {
+                try {
+                    var json = File.ReadAllText(path);
+                    var asmdef = JsonConvert.DeserializeObject<AssemblyDefinitionJson>(json);
+                    return asmdef;
+                } catch (Exception) {
+                    // ignore malformed asmdef
+                    return null;
+                }
+            }
+
+            // Unity Define Constraints syntax is described in the docs https://docs.unity3d.com/Manual/class-AssemblyDefinitionImporter.html
+            static readonly Dictionary<string, string> syntaxMap = new Dictionary<string, string> {
+                    { "OR", "||" },
+                    { "AND", "&&" },
+                    { "NOT", "!" }
+                };
+            
+            
+            /// <summary>
+            /// Evaluate a define constraint like 'UNITY_ANDROID || UNITY_IOS'
+            /// </summary>
+            /// <param name="input"></param>
+            /// <param name="defineSymbols"></param>
+            /// <returns></returns>
+            public static bool EvaluateDefineConstraint(string input, string[] defineSymbols) {
+                // map Unity defineConstraints syntax to DataTable syntax (unity supports both)
+                foreach (var item in syntaxMap) {
+                    // surround with space because || may not have spaces around it
+                    input = input.Replace(item.Value, $" {item.Key} ");
+                }
+
+                // remove any extra spaces we just created
+                input = input.Replace("  ", " ");
+
+                var tokens = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+
+                foreach (var token in tokens) {
+                    if (!syntaxMap.ContainsKey(token) && token != "false" && token != "true") {
+                        var index = input.IndexOf(token, StringComparison.Ordinal);
+                        
+                        // replace symbols with true or false depending if they are in the array or not.
+                        input = input.Substring(0, index) + defineSymbols.Contains(token) + input.Substring(index + token.Length);
+                    }
+                }
+
+                var dt = new DataTable();
+                return (bool)dt.Compute(input, "");
+            }
+        }
+    }
+
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/AssemblyOmission.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0b94f2314a044b109de488be1ccd5640
+timeCreated: 1674233674

+ 144 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/BuildInfoHelper.cs

@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using UnityEditor;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+    struct BuildInfoInput {
+        public readonly string allDefineSymbols;
+        public readonly BuildTarget activeBuildTarget;
+        public readonly string[] omittedProjects;
+        public readonly bool batchMode;
+
+        public BuildInfoInput(string allDefineSymbols, BuildTarget activeBuildTarget, string[] omittedProjects, bool batchMode) {
+            this.allDefineSymbols = allDefineSymbols;
+            this.activeBuildTarget = activeBuildTarget;
+            this.omittedProjects = omittedProjects;
+            this.batchMode = batchMode;
+        }
+    }
+    
+    static class BuildInfoHelper {
+        public static async Task<BuildInfoInput> GetGenerateBuildInfoInput() {
+            var buildTarget = EditorUserBuildSettings.activeBuildTarget;
+            var activeDefineSymbols = EditorUserBuildSettings.activeScriptCompilationDefines;
+            var batchMode = Application.isBatchMode;
+            var allDefineSymbols = await Task.Run(() => {
+                return GetAllAndroidMonoBuildDefineSymbolsThreaded(activeDefineSymbols);
+            });
+            // cached so unexpensive most of the time
+            var omittedProjects = AssemblyOmission.GetOmittedProjects(allDefineSymbols);
+
+            return new BuildInfoInput(
+                allDefineSymbols: allDefineSymbols,
+                activeBuildTarget: buildTarget,
+                omittedProjects: omittedProjects,
+                batchMode: batchMode
+            );
+        }
+
+        public static BuildInfo GenerateBuildInfoMainThread() {
+            return GenerateBuildInfoMainThread(EditorUserBuildSettings.activeBuildTarget);
+        }
+        
+        public static BuildInfo GenerateBuildInfoMainThread(BuildTarget buildTarget) {
+            var allDefineSymbols = GetAllAndroidMonoBuildDefineSymbolsThreaded(EditorUserBuildSettings.activeScriptCompilationDefines);
+            return GenerateBuildInfoThreaded(new BuildInfoInput(
+                allDefineSymbols: allDefineSymbols, 
+                activeBuildTarget: buildTarget, 
+                omittedProjects: AssemblyOmission.GetOmittedProjects(allDefineSymbols),
+                batchMode: Application.isBatchMode
+            ));
+        }
+
+        public static BuildInfo GenerateBuildInfoThreaded(BuildInfoInput input) {
+            var omittedProjectRegex = String.Join("|", input.omittedProjects.Select(name => Regex.Escape(name)));
+            var shortCommitHash = GitUtil.GetShortCommitHashOrFallback();
+            var hostname = IsHumanControllingUs(input.batchMode) ? IpHelper.GetIpAddress() : null; 
+            
+            //  Note: add a string to uniquely identify the Unity project. Could use filepath to /MyProject/Assets/ (editor Application.dataPath)
+            //  or application identifier (com.company.appname).
+            //  Do this when supporting multiple projects: SG-28807
+            //  The matching code is in Runtime assembly which compares server response with built BuildInfo.
+            return new BuildInfo {
+                projectIdentifier = "SG-29580",
+                commitHash = shortCommitHash,
+                defineSymbols = input.allDefineSymbols, 
+                projectOmissionRegex = omittedProjectRegex,
+                buildMachineHostName = hostname,
+                buildMachinePort = RequestHelper.port,
+                activeBuildTarget = input.activeBuildTarget.ToString(),
+                buildMachineRequestOrigin = RequestHelper.origin,
+            };
+        }
+
+        public static bool IsHumanControllingUs(bool batchMode) {
+            if (batchMode) {
+                return false;
+            }
+
+            var isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
+            return !isCI;
+        }
+
+        private static readonly string[] editorSymbolsToRemove = {
+            "PLATFORM_ARCH_64",
+            "UNITY_64",
+            "UNITY_INCLUDE_TESTS",
+            "UNITY_EDITOR",
+            "UNITY_EDITOR_64",
+            "UNITY_EDITOR_WIN",
+            "ENABLE_UNITY_COLLECTIONS_CHECKS",
+            "ENABLE_BURST_AOT",
+            "RENDER_SOFTWARE_CURSOR",
+            "PLATFORM_STANDALONE_WIN",
+            "PLATFORM_STANDALONE",
+            "UNITY_STANDALONE_WIN",
+            "UNITY_STANDALONE",
+            "ENABLE_MOVIES",
+            "ENABLE_OUT_OF_PROCESS_CRASH_HANDLER",
+            "ENABLE_WEBSOCKET_HOST",
+            "ENABLE_CLUSTER_SYNC",
+            "ENABLE_CLUSTERINPUT",
+        };
+
+        private static readonly string[] androidSymbolsToAdd = { 
+            "CSHARP_7_OR_LATER",
+            "CSHARP_7_3_OR_NEWER",
+            "PLATFORM_ANDROID",
+            "UNITY_ANDROID",
+            "UNITY_ANDROID_API",
+            "ENABLE_EGL",
+            "DEVELOPMENT_BUILD",
+            "ENABLE_CLOUD_SERVICES_NATIVE_CRASH_REPORTING",
+            "PLATFORM_SUPPORTS_ADS_ID",
+            "UNITY_CAN_SHOW_SPLASH_SCREEN",
+            "UNITY_HAS_GOOGLEVR",
+            "UNITY_HAS_TANGO",
+            "ENABLE_SPATIALTRACKING",
+            "ENABLE_RUNTIME_PERMISSIONS",
+            "ENABLE_ENGINE_CODE_STRIPPING",
+            "UNITY_ASTC_ONLY_DECOMPRESS",
+            "ANDROID_USE_SWAPPY",
+            "ENABLE_ONSCREEN_KEYBOARD",
+            "ENABLE_UNITYADS_RUNTIME",
+            "UNITY_UNITYADS_API",
+        };
+        
+        // Currently there is no better way. Alternatively we could hook into unity's call to csc.exe and parse the /define: arguments. 
+        //   Hardcoding the differences was less effort and is less error prone.
+        // I also looked into it and tried all the Build interfaces like this one https://docs.unity3d.com/ScriptReference/Build.IPostBuildPlayerScriptDLLs.html
+        //   and logging EditorUserBuildSettings.activeScriptCompilationDefines in the callbacks - result: all same like editor, so I agree that hardcode is best. 
+        public static string GetAllAndroidMonoBuildDefineSymbolsThreaded(string[] defineSymbols) {
+            var defines = new HashSet<string>(defineSymbols);
+            defines.ExceptWith(editorSymbolsToRemove);
+            defines.UnionWith(androidSymbolsToAdd);
+            // sort for consistency, must be deterministic
+            var definesArray = defines.OrderBy(def => def).ToArray();
+            return String.Join(";", definesArray);
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/BuildInfoHelper.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: f41ad09ae4f04088bf6c9ad9a4fc0885
+timeCreated: 1674220023

+ 101 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/EditorWindowHelper.cs

@@ -0,0 +1,101 @@
+using System;
+using System.Text.RegularExpressions;
+using UnityEngine;
+using System.Threading.Tasks;
+using UnityEditor;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SingularityGroup.HotReload.Editor {
+    internal static class EditorWindowHelper {
+        #if UNITY_2020_1_OR_NEWER
+        public static bool supportsNotifications = true;
+        #else
+        public static bool supportsNotifications = false;
+        #endif
+        
+        private static readonly Regex ValidEmailRegex = new Regex(@"^(?!\.)(""([^""\r\\]|\\[""\r\\])*""|"
+            + @"([-a-z0-9!#$%&'*+/=?^_`{|}~]|(?<!\.)\.)*)(?<!\.)"
+            + @"@[a-z0-9][\w\.-]*[a-z0-9]\.[a-z][a-z\.]*[a-z]$", RegexOptions.IgnoreCase);
+
+        public static bool IsValidEmailAddress(string email) {
+            return ValidEmailRegex.IsMatch(email);
+        }
+
+        public static bool IsHumanControllingUs() {
+            if (Application.isBatchMode) {
+                return false;
+            }
+            
+            var isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
+            return !isCI;
+        }
+
+        internal enum NotificationStatus {  
+            None,
+            Patching,
+            NeedsRecompile
+        }
+
+        private static readonly Dictionary<NotificationStatus, GUIContent> notificationContent = new Dictionary<NotificationStatus, GUIContent> {
+            { NotificationStatus.Patching, new GUIContent("[Hot Reload] Applying patches...")},
+            { NotificationStatus.NeedsRecompile, new GUIContent("[Hot Reload] Unsupported Changes detected! Recompiling...")},
+        };
+        
+        static Type gameViewT;
+        private static EditorWindow[] gameViewWindows {
+            get {
+                gameViewT = gameViewT ?? typeof(EditorWindow).Assembly.GetType("UnityEditor.GameView");
+                return Resources.FindObjectsOfTypeAll(gameViewT).Cast<EditorWindow>().ToArray();
+            }
+        }
+
+        private static EditorWindow[] sceneWindows {
+            get {
+                return Resources.FindObjectsOfTypeAll(typeof(SceneView)).Cast<EditorWindow>().ToArray();
+            }
+        }
+
+        private static EditorWindow[] notificationWindows {
+            get {
+                return gameViewWindows.Concat(sceneWindows).ToArray();
+            }
+        }
+
+        static NotificationStatus lastNotificationStatus;
+        private static DateTime? latestNotificationStartedAt;
+        private static bool notificationShownRecently => latestNotificationStartedAt != null && DateTime.UtcNow - latestNotificationStartedAt < TimeSpan.FromSeconds(1);
+        internal static void ShowNotification(NotificationStatus notificationType, float maxDuration = 3) {
+            // Patch status goes from Unsupported changes to patching rapidly when making unsupported change
+            // patching also shows right before unsupported changes sometimes 
+            // so we don't override NeedsRecompile notification ever
+            bool willOverrideNeedsCompileNotification = notificationType != NotificationStatus.NeedsRecompile && notificationShownRecently || lastNotificationStatus == NotificationStatus.NeedsRecompile && notificationShownRecently;
+            if (!supportsNotifications || willOverrideNeedsCompileNotification) {
+                return;
+            }
+
+            foreach (EditorWindow notificationWindow in notificationWindows) {
+                notificationWindow.ShowNotification(notificationContent[notificationType], maxDuration);
+                notificationWindow.Repaint();
+            }
+            latestNotificationStartedAt = DateTime.UtcNow;
+            lastNotificationStatus = notificationType;
+        }
+
+        internal static void RemoveNotification() {
+            if (!supportsNotifications) {
+                return;
+            }
+            // only patching notifications should be removed after showing less than 1 second
+            if (notificationShownRecently && lastNotificationStatus != NotificationStatus.Patching) {
+                return;
+            }
+            foreach (EditorWindow notificationWindow in notificationWindows) {                
+                notificationWindow.RemoveNotification(); 
+                notificationWindow.Repaint();
+            }   
+            latestNotificationStartedAt = null;    
+            lastNotificationStatus = NotificationStatus.None;
+        }
+    }
+}

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/EditorWindowHelper.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fd463b1f0bfddf34caa662ebe375e5fe
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 162 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/GUIHelper.cs

@@ -0,0 +1,162 @@
+using UnityEngine;
+using System.Collections.Generic;
+
+namespace SingularityGroup.HotReload.Editor {
+    internal enum InvertibleIcon {
+        BugReport,
+        Events,
+        EventsNew,
+        Recompile,
+        Logo,
+        Close,
+        FoldoutOpen,
+        FoldoutClosed,
+        Spinner,
+        Stop,
+        Start,
+    }
+    
+    internal static class GUIHelper {
+        private static readonly Dictionary<InvertibleIcon, string> supportedInvertibleIcons = new Dictionary<InvertibleIcon, string> {
+            { InvertibleIcon.BugReport, "report_bug" },
+            { InvertibleIcon.Events, "events" },
+            { InvertibleIcon.Recompile, "refresh" },
+            { InvertibleIcon.Logo, "logo" },
+            { InvertibleIcon.Close, "close" },
+            { InvertibleIcon.FoldoutOpen, "foldout_open" },
+            { InvertibleIcon.FoldoutClosed, "foldout_closed" },
+            { InvertibleIcon.Spinner, "icon_loading_star_light_mode_96" },
+            { InvertibleIcon.Stop, "Icn_Stop" },
+            { InvertibleIcon.Start, "Icn_play" },
+        };
+        
+        private static readonly Dictionary<InvertibleIcon, Texture2D> invertibleIconCache = new Dictionary<InvertibleIcon, Texture2D>();
+        private static readonly Dictionary<InvertibleIcon, Texture2D> invertibleIconInvertedCache = new Dictionary<InvertibleIcon, Texture2D>();
+        private static readonly Dictionary<string, Texture2D> iconCache = new Dictionary<string, Texture2D>();
+        
+        internal static Texture2D InvertTextureColor(Texture2D originalTexture) {
+            if (!originalTexture) {
+                return originalTexture;
+            }
+            // Get the original pixels from the texture
+            Color[] originalPixels = originalTexture.GetPixels();
+
+            // Create a new array for the inverted colors
+            Color[] invertedPixels = new Color[originalPixels.Length];
+
+            // Iterate through the pixels and invert the colors while preserving the alpha channel
+            for (int i = 0; i < originalPixels.Length; i++) {
+                Color originalColor = originalPixels[i];
+                Color invertedColor = new Color(1 - originalColor.r, 1 - originalColor.g, 1 - originalColor.b, originalColor.a);
+                invertedPixels[i] = invertedColor;
+            }
+
+            // Create a new texture and set its pixels
+            Texture2D invertedTexture = new Texture2D(originalTexture.width, originalTexture.height);
+            invertedTexture.SetPixels(invertedPixels);
+
+            // Apply the changes to the texture
+            invertedTexture.Apply();
+
+            return invertedTexture;
+        }
+
+        internal static Texture2D GetInvertibleIcon(InvertibleIcon invertibleIcon) {
+            Texture2D iconTexture;
+            var cache = HotReloadWindowStyles.IsDarkMode ? invertibleIconInvertedCache : invertibleIconCache;
+           
+            if (!cache.TryGetValue(invertibleIcon, out iconTexture) || !iconTexture) {
+                var type = invertibleIcon == InvertibleIcon.EventsNew ? InvertibleIcon.Events : invertibleIcon;
+                iconTexture = Resources.Load<Texture2D>(supportedInvertibleIcons[type]);
+                
+                // we assume icons are for light mode by default
+                // therefore if its dark mode we should invert them
+                if (HotReloadWindowStyles.IsDarkMode) {
+                    iconTexture = InvertTextureColor(iconTexture);
+                }
+
+                cache[type] = iconTexture;
+
+                // we combine dot image with Events icon to create a new alert version
+                if (invertibleIcon == InvertibleIcon.EventsNew) {
+                    var redDot = Resources.Load<Texture2D>("red_dot");
+                    iconTexture = CombineImages(iconTexture, redDot);
+                    cache[InvertibleIcon.EventsNew] = iconTexture;
+                }
+            }
+            return cache[invertibleIcon];
+        }
+        
+        internal static Texture2D GetLocalIcon(string iconName) {
+            Texture2D iconTexture;
+            if (!iconCache.TryGetValue(iconName, out iconTexture) || !iconTexture) {
+                iconTexture = Resources.Load<Texture2D>(iconName);
+                iconCache[iconName] = iconTexture;
+            }
+            return iconTexture;
+        }
+        
+        static Texture2D CombineImages(Texture2D image1, Texture2D image2) {
+            if (!image1 || !image2) {
+                return image1;
+            }
+            var combinedImage = new Texture2D(Mathf.Max(image1.width, image2.width), Mathf.Max(image1.height, image2.height));
+
+            for (int y = 0; y < combinedImage.height; y++) {
+                for (int x = 0; x < combinedImage.width; x++) {
+                    Color color1 = x < image1.width && y < image1.height ? image1.GetPixel(x, y) : Color.clear;
+                    Color color2 = x < image2.width && y < image2.height ? image2.GetPixel(x, y) : Color.clear;
+                    combinedImage.SetPixel(x, y, Color.Lerp(color1, color2, color2.a));
+                }
+            }
+            combinedImage.Apply();
+            return combinedImage;
+        }
+        
+        private static readonly Dictionary<Color, Texture2D> textureColorCache = new Dictionary<Color, Texture2D>();
+        internal static Texture2D ConvertTextureToColor(Color color) {
+            Texture2D texture;
+            if (!textureColorCache.TryGetValue(color, out texture) || !texture) {
+                texture = new Texture2D(1, 1);
+                texture.SetPixel(0, 0, color);
+                texture.Apply();
+                textureColorCache[color] = texture;
+            }
+            return texture;
+        }
+        
+        private static readonly Dictionary<string, Texture2D> grayTextureCache = new Dictionary<string, Texture2D>();
+        private static readonly Dictionary<string, Color> colorFactor = new Dictionary<string, Color> {
+            { "error", new Color(0.6f, 0.587f, 0.114f) },
+        };
+        
+        internal static Texture2D ConvertToGrayscale(string localIcon) {
+            Texture2D _texture;
+            if (!grayTextureCache.TryGetValue(localIcon, out _texture) || !_texture) {
+                var icon = GUIHelper.GetLocalIcon(localIcon);
+                // Create a copy of the texture
+                Texture2D copiedTexture = new Texture2D(icon.width, icon.height, TextureFormat.RGBA32, false);
+
+                // Convert the copied texture to grayscale
+                Color[] pixels = icon.GetPixels();
+                for (int i = 0; i < pixels.Length; i++) {
+                    Color pixel = pixels[i];
+                    Color factor;
+                    if (!colorFactor.TryGetValue(localIcon, out factor)) {
+                        factor = new Color(0.299f, 0.587f, 0.114f);
+                    }
+                    float grayscale = factor.r * pixel.r + factor.g * pixel.g + factor.b * pixel.b;
+                    pixels[i] = new Color(grayscale, grayscale, grayscale, pixel.a);  // Preserve alpha channel
+                }
+                copiedTexture.SetPixels(pixels);
+                copiedTexture.Apply();
+
+                // Store the grayscale texture in the cache
+                grayTextureCache[localIcon] = copiedTexture;
+
+                return copiedTexture;
+            }
+            return _texture;
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/GUIHelper.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b4be912211814333ab61898b6440dc8e
+timeCreated: 1694518358

+ 544 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/HotReloadSuggestionsHelper.cs

@@ -0,0 +1,544 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using SingularityGroup.HotReload.DTO;
+using UnityEditor;
+using UnityEditor.Compilation;
+using UnityEditor.PackageManager;
+using UnityEditor.PackageManager.Requests;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+
+	internal static class HotReloadSuggestionsHelper {
+        internal static void SetSuggestionsShown(HotReloadSuggestionKind hotReloadSuggestionKind) {
+            if (EditorPrefs.GetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}")) {
+                return;
+            }
+            EditorPrefs.SetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}", true);
+            EditorPrefs.SetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}", true);
+            AlertEntry entry;
+            if (suggestionMap.TryGetValue(hotReloadSuggestionKind, out entry) && !HotReloadTimelineHelper.Suggestions.Contains(entry)) {
+                HotReloadTimelineHelper.Suggestions.Insert(0, entry);
+                HotReloadState.ShowingRedDot = true;
+            }
+        }
+        
+        internal static bool CheckSuggestionActive(HotReloadSuggestionKind hotReloadSuggestionKind) {
+            return EditorPrefs.GetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}");
+        }
+        
+        internal static bool CheckSuggestionShown(HotReloadSuggestionKind hotReloadSuggestionKind) {
+            return EditorPrefs.GetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}");
+        }
+
+        internal static bool CanShowServerSuggestion(HotReloadSuggestionKind hotReloadSuggestionKind) {
+            if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerWithSideEffects) {
+                return !HotReloadState.ShowedFieldInitializerWithSideEffects;
+            } else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited) {
+                return !HotReloadState.ShowedFieldInitializerExistingInstancesEdited;
+            } else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited) {
+                return !HotReloadState.ShowedFieldInitializerExistingInstancesUnedited;
+            } else if (hotReloadSuggestionKind == HotReloadSuggestionKind.AddMonobehaviourMethod) {
+                return !HotReloadState.ShowedAddMonobehaviourMethods;
+            } else if (hotReloadSuggestionKind == HotReloadSuggestionKind.DetailedErrorReportingIsEnabled) {
+                return !CheckSuggestionShown(HotReloadSuggestionKind.DetailedErrorReportingIsEnabled);
+            }
+            return false;
+        }
+        
+        internal static void SetServerSuggestionShown(HotReloadSuggestionKind hotReloadSuggestionKind) {
+            if (hotReloadSuggestionKind == HotReloadSuggestionKind.DetailedErrorReportingIsEnabled) {
+                HotReloadSuggestionsHelper.SetSuggestionsShown(hotReloadSuggestionKind);
+                return;
+            } 
+            if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerWithSideEffects) {
+                HotReloadState.ShowedFieldInitializerWithSideEffects = true;
+            } else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited) {
+                HotReloadState.ShowedFieldInitializerExistingInstancesEdited = true;
+            } else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited) {
+                HotReloadState.ShowedFieldInitializerExistingInstancesUnedited = true;
+            } else if (hotReloadSuggestionKind == HotReloadSuggestionKind.AddMonobehaviourMethod) {
+                HotReloadState.ShowedAddMonobehaviourMethods = true;
+            } else {
+                return;
+            }
+            HotReloadSuggestionsHelper.SetSuggestionActive(hotReloadSuggestionKind);
+        }
+        
+        // used for cases where suggestion might need to be shown more than once
+        internal static void SetSuggestionActive(HotReloadSuggestionKind hotReloadSuggestionKind) {
+            if (EditorPrefs.GetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}")) {
+                return;
+            }
+            EditorPrefs.SetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}", true);
+            
+            AlertEntry entry;
+            if (suggestionMap.TryGetValue(hotReloadSuggestionKind, out entry) && !HotReloadTimelineHelper.Suggestions.Contains(entry)) {
+                HotReloadTimelineHelper.Suggestions.Insert(0, entry);
+                HotReloadState.ShowingRedDot = true;
+            }
+        }
+        
+        internal static void SetSuggestionInactive(HotReloadSuggestionKind hotReloadSuggestionKind) {
+            EditorPrefs.SetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}", false);
+            AlertEntry entry;
+            if (suggestionMap.TryGetValue(hotReloadSuggestionKind, out entry)) {
+                HotReloadTimelineHelper.Suggestions.Remove(entry);
+            }
+        }
+        
+        internal static void InitSuggestions() {
+            foreach (HotReloadSuggestionKind value in Enum.GetValues(typeof(HotReloadSuggestionKind))) {
+                if (!CheckSuggestionActive(value)) {
+                    continue;
+                }
+                AlertEntry entry;
+                if (suggestionMap.TryGetValue(value, out entry) && !HotReloadTimelineHelper.Suggestions.Contains(entry)) {
+                    HotReloadTimelineHelper.Suggestions.Insert(0, entry);
+                }
+            }
+        }
+        
+        internal static HotReloadSuggestionKind? FindSuggestionKind(AlertEntry targetEntry) {
+            foreach (KeyValuePair<HotReloadSuggestionKind, AlertEntry> pair in suggestionMap) {
+                if (pair.Value.Equals(targetEntry)) {
+                    return pair.Key;
+                }
+            }
+            return null;
+        }
+        
+        internal static readonly OpenURLButton recompileTroubleshootingButton = new OpenURLButton("Docs", Constants.RecompileTroubleshootingURL);
+        internal static readonly OpenURLButton featuresDocumentationButton = new OpenURLButton("Docs", Constants.FeaturesDocumentationURL);
+        internal static readonly OpenURLButton multipleEditorsDocumentationButton = new OpenURLButton("Docs", Constants.MultipleEditorsURL);
+        internal static readonly OpenURLButton debuggerDocumentationButton = new OpenURLButton("More Info", Constants.DebuggerURL);
+        public static Dictionary<HotReloadSuggestionKind, AlertEntry> suggestionMap = new Dictionary<HotReloadSuggestionKind, AlertEntry> {
+            { HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023, new AlertEntry(
+                AlertType.Suggestion, 
+                "Vote for the \"Best Development Tool\" Award!", 
+                "Hot Reload was nominated for the \"Best Development Tool\" Award. Please consider voting. Thank you!",
+                actionData: () => {
+                    GUILayout.Space(6f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        if (GUILayout.Button(" Vote ")) {
+                            Application.OpenURL(Constants.VoteForAwardURL);
+                            SetSuggestionInactive(HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023);
+                        }
+                        GUILayout.FlexibleSpace();
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout
+            )},
+            { HotReloadSuggestionKind.UnsupportedChanges, new AlertEntry(
+                AlertType.Suggestion, 
+                "Which changes does Hot Reload support?", 
+                "Hot Reload supports most code changes, but there are some limitations. Generally, changes to methods and fields are supported. Things like adding new types is not (yet) supported. See the documentation for the list of current features and our current roadmap",
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        featuresDocumentationButton.OnGUI();
+                        GUILayout.FlexibleSpace();
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout
+            )},
+            { HotReloadSuggestionKind.UnsupportedPackages, new AlertEntry(
+                AlertType.Suggestion, 
+                "Unsupported package detected",
+                "The following packages are only partially supported: ECS, Mirror, Fishnet, and Photon. Hot Reload will work in the project, but changes specific to those packages might not hot-reload",
+                iconType: AlertType.UnsupportedChange,
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        HotReloadAboutTab.contactButton.OnGUI();
+                        GUILayout.FlexibleSpace();
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout
+            )},
+            { HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges, new AlertEntry(
+                AlertType.Suggestion, 
+                "Unity recompiles on enter/exit play mode?",
+                "If you have an issue with the Unity Editor recompiling when the Play Mode state changes, more info is available in the docs. Feel free to reach out if you require assistance. We'll be glad to help.",
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        recompileTroubleshootingButton.OnGUI();
+                        GUILayout.Space(5f);
+                        HotReloadAboutTab.discordButton.OnGUI();
+                        GUILayout.Space(5f);
+                        HotReloadAboutTab.contactButton.OnGUI();
+                        GUILayout.FlexibleSpace();
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout
+            )},
+#if UNITY_2022_1_OR_NEWER
+            { HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022, new AlertEntry(
+                AlertType.Suggestion, 
+                "Unsupported setting detected",
+                "The 'Sprite Packer Mode' setting can cause unintended recompilations if set to 'Sprite Atlas V1 - Always Enabled'",
+                iconType: AlertType.UnsupportedChange,
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        if (GUILayout.Button(" Use \"Build Time Only Atlas\" ")) {
+                            if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2) {
+                                EditorSettings.spritePackerMode = SpritePackerMode.SpriteAtlasV2Build;
+                            } else {
+                                EditorSettings.spritePackerMode = SpritePackerMode.BuildTimeOnlyAtlas;
+                            }
+                        }
+                        if (GUILayout.Button(" Open Settings ")) {
+                            SettingsService.OpenProjectSettings("Project/Editor");
+                        }
+                        if (GUILayout.Button(" Ignore suggestion ")) {
+                            SetSuggestionInactive(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
+                        }
+                        
+                        GUILayout.FlexibleSpace();
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout,
+                hasExitButton: false
+            )},
+#endif
+            { HotReloadSuggestionKind.MultidimensionalArrays, new AlertEntry(
+                AlertType.Suggestion, 
+                "Use jagged instead of multidimensional arrays",
+                "Hot Reload doesn't support methods with multidimensional arrays ([,]). You can work around this by using jagged arrays ([][])",
+                iconType: AlertType.UnsupportedChange,
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        if (GUILayout.Button(" Learn more ")) {
+                            Application.OpenURL("https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1814");
+                        }
+                        GUILayout.FlexibleSpace();
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout
+            )},
+            { HotReloadSuggestionKind.EditorsWithoutHRRunning, new AlertEntry(
+                AlertType.Suggestion, 
+                "Some Unity instances don't have Hot Reload running.",
+                "Make sure that either: \n1) Hot Reload is installed and running on all Editor instances, or \n2) Hot Reload is stopped in all Editor instances where it is installed.",
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        if (GUILayout.Button(" Stop Hot Reload ")) {
+                            EditorCodePatcher.StopCodePatcher().Forget();
+                        }
+                        GUILayout.Space(5f);
+                        
+                        multipleEditorsDocumentationButton.OnGUI();
+                        GUILayout.Space(5f);
+                        
+                        if (GUILayout.Button(" Don't show again ")) {
+                            HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.EditorsWithoutHRRunning);
+                            HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
+                        }
+                        GUILayout.FlexibleSpace();
+                        GUILayout.FlexibleSpace();
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout,
+                iconType: AlertType.UnsupportedChange
+            )},
+            // Not in use (never reported from the server)
+            { HotReloadSuggestionKind.FieldInitializerWithSideEffects, new AlertEntry(
+                AlertType.Suggestion, 
+                "Field initializer with side-effects detected",
+                "A field initializer update might have side effects, e.g. calling a method or creating an object.\n\nWhile Hot Reload does support this, it can sometimes be confusing when the initializer logic runs at 'unexpected times'.",
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        if (GUILayout.Button(" OK ")) {
+                            SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
+                        }
+                        GUILayout.FlexibleSpace();
+                        if (GUILayout.Button(" Don't show again ")) {
+                            SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
+                            SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
+                        }
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout,
+                iconType: AlertType.Suggestion
+            )},
+            { HotReloadSuggestionKind.DetailedErrorReportingIsEnabled, new AlertEntry(
+                AlertType.Suggestion, 
+                "Detailed error reporting is enabled",
+                "When an error happens in Hot Reload, the exception stacktrace is sent as telemetry to help diagnose and fix the issue.\nThe exception stack trace is only included if it originated from the Hot Reload package or binary. Stacktraces from your own code are not sent.\nYou can disable detailed error reporting to prevent telemetry from including any information about your project.",
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        GUILayout.Space(4f);
+                        if (GUILayout.Button("    OK    ")) {
+                            SetSuggestionInactive(HotReloadSuggestionKind.DetailedErrorReportingIsEnabled);
+                        }
+                        GUILayout.FlexibleSpace();
+                        if (GUILayout.Button(" Disable ")) {
+                            HotReloadSettingsTab.DisableDetailedErrorReportingInner(true);
+                            SetSuggestionInactive(HotReloadSuggestionKind.DetailedErrorReportingIsEnabled);
+                        }
+                        GUILayout.Space(10f);
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout,
+                iconType: AlertType.Suggestion
+            )},
+            // Not in use (never reported from the server)
+            { HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited, new AlertEntry(
+                AlertType.Suggestion, 
+                "Field initializer edit updated the value of existing class instances",
+                "By default, Hot Reload updates field values of existing object instances when new field initializer has constant value.\n\nIf you want to change this behavior, disable the \"Apply field initializer edits to existing class instances\" option in Settings or click the button below.",
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        if (GUILayout.Button(" Turn off ")) {
+                            #pragma warning disable CS0618
+                            HotReloadSettingsTab.ApplyApplyFieldInitializerEditsToExistingClassInstances(false);
+                            #pragma warning restore CS0618
+                            SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
+                        }
+                        if (GUILayout.Button(" Open Settings ")) {
+                            HotReloadWindow.Current.SelectTab(typeof(HotReloadSettingsTab));
+                        }
+                        GUILayout.FlexibleSpace();
+                        if (GUILayout.Button(" Don't show again ")) {
+                            SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
+                            SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
+                        }
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout,
+                iconType: AlertType.Suggestion
+            )},
+            { HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited, new AlertEntry(
+                AlertType.Suggestion, 
+                "Field initializer edits don't apply to existing objects",
+                "By default, Hot Reload applies field initializer edits of existing fields only to new objects (newly instantiated classes), just like normal C#.\n\nFor rapid prototyping, you can use static fields which will update across all instances.",
+                actionData: () => {
+                    GUILayout.Space(8f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        if (GUILayout.Button(" OK ")) {
+                            SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited);
+                            SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited);
+                        }
+                        GUILayout.FlexibleSpace();
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout,
+                iconType: AlertType.Suggestion
+            )},
+            { HotReloadSuggestionKind.AddMonobehaviourMethod, new AlertEntry(
+                AlertType.Suggestion, 
+                "New MonoBehaviour methods are not shown in the inspector",
+                "New methods in MonoBehaviours are not shown in the inspector until the script is recompiled. This is a limitation of Hot Reload handling of Unity's serialization system.\n\nYou can use the button below to auto recompile partially supported changes such as this one.",
+                actionData: () => {
+                    GUILayout.Space(8f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        if (GUILayout.Button(" OK ")) {
+                            SetSuggestionInactive(HotReloadSuggestionKind.AddMonobehaviourMethod);
+                        }
+                        if (GUILayout.Button(" Auto Recompile ")) {
+                            SetSuggestionInactive(HotReloadSuggestionKind.AddMonobehaviourMethod);
+                            HotReloadPrefs.AutoRecompilePartiallyUnsupportedChanges = true;
+                            HotReloadPrefs.DisplayNewMonobehaviourMethodsAsPartiallySupported = true;
+                            HotReloadRunTab.RecompileWithChecks();
+                        }
+                        GUILayout.FlexibleSpace();
+                        if (GUILayout.Button(" Don't show again ")) {
+                            SetSuggestionsShown(HotReloadSuggestionKind.AddMonobehaviourMethod);
+                            SetSuggestionInactive(HotReloadSuggestionKind.AddMonobehaviourMethod);
+                        }
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout,
+                iconType: AlertType.Suggestion
+            )},
+#if UNITY_2020_1_OR_NEWER
+            { HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods, new AlertEntry(
+                AlertType.Suggestion, 
+                "Switch code optimization to Debug Mode",
+                "In Release Mode some methods are inlined, which prevents Hot Reload from applying changes. A clear warning is always shown when this happens, but you can use Debug Mode to avoid the issue altogether",
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        if (GUILayout.Button(" Switch to Debug mode ") && HotReloadRunTab.ConfirmExitPlaymode("Switching code optimization will stop Play Mode.\n\nDo you wish to proceed?")) {
+                            HotReloadRunTab.SwitchToDebugMode();
+                        }
+                        GUILayout.FlexibleSpace();
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout,
+                iconType: AlertType.UnsupportedChange
+            )},
+#endif
+            { HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached, new AlertEntry(
+                AlertType.Suggestion, 
+                "Hot Reload is disabled while a debugger is attached",
+                "Hot Reload automatically disables itself while a debugger is attached, as it can otherwise interfere with certain debugger features.\nWhile disabled, every code change will trigger a full Unity recompilation.\n\nYou can choose to keep Hot Reload enabled while a debugger is attached, though some features like debugger variable inspection might not always work as expected.",
+                actionData: () => {
+                    GUILayout.Space(8f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        if (GUILayout.Button(" Keep enabled during debugging ")) {
+                            SetSuggestionInactive(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
+                            HotReloadPrefs.AutoDisableHotReloadWithDebugger = false;
+                        }
+                        GUILayout.FlexibleSpace();
+                        debuggerDocumentationButton.OnGUI();
+                        if (GUILayout.Button(" Don't show again ")) {
+                            SetSuggestionsShown(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
+                            SetSuggestionInactive(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
+                        }
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout,
+                iconType: AlertType.Suggestion
+            )},
+            { HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached, new AlertEntry(
+                AlertType.Suggestion, 
+                "Hot Reload may interfere with your debugger session",
+                "Some debugger features, like variable inspection, might not work as expected for methods patched during the Hot Reload session. A full Unity recompile is required to get the full debugger experience.",
+                actionData: () => {
+                    GUILayout.Space(8f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        if (GUILayout.Button(" Recompile ")) {
+                            SetSuggestionInactive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached);
+                            if (HotReloadRunTab.ConfirmExitPlaymode("Using the Recompile button will stop Play Mode.\n\nDo you wish to proceed?")) {
+                                HotReloadRunTab.Recompile();
+                            }
+                        }
+                        GUILayout.FlexibleSpace();
+                        debuggerDocumentationButton.OnGUI();
+                        GUILayout.Space(8f);
+                    }
+                },
+                timestamp: DateTime.Now,
+                entryType: EntryType.Foldout,
+                iconType: AlertType.UnsupportedChange,
+                hasExitButton: false
+            )},
+            
+        };
+        
+        static ListRequest listRequest;
+        static string[] unsupportedPackages = new[] {
+            "com.unity.entities",
+            "com.firstgeargames.fishnet",
+        };
+        static List<string> unsupportedPackagesList;
+        static DateTime lastPlaymodeChange;
+        
+        public static void Init() {
+            listRequest = Client.List(offlineMode: false, includeIndirectDependencies: true);
+
+            EditorApplication.playModeStateChanged += state => {
+                lastPlaymodeChange = DateTime.UtcNow;
+            };
+            CompilationPipeline.compilationStarted += obj => {
+                if (DateTime.UtcNow - lastPlaymodeChange < TimeSpan.FromSeconds(1) && !HotReloadState.RecompiledUnsupportedChangesOnExitPlaymode) {
+                    
+#if UNITY_2022_1_OR_NEWER
+                    SetSuggestionsShown(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
+#else
+                    SetSuggestionsShown(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges);
+#endif
+                }
+                HotReloadState.RecompiledUnsupportedChangesOnExitPlaymode = false;
+            };
+            InitSuggestions();
+        }
+
+        private static DateTime lastCheckedUnityInstances = DateTime.UtcNow;
+        public static void Check() {
+            if (listRequest.IsCompleted && 
+                unsupportedPackagesList == null) 
+            {
+                unsupportedPackagesList = new List<string>();
+                if (listRequest.Result != null) {
+                    foreach (var packageInfo in listRequest.Result) {
+                        if (unsupportedPackages.Contains(packageInfo.name)) {
+                            unsupportedPackagesList.Add(packageInfo.name);
+                        }
+                    }
+                }
+                if (unsupportedPackagesList.Count > 0) {
+                    SetSuggestionsShown(HotReloadSuggestionKind.UnsupportedPackages);
+                }
+            }
+            
+            CheckEditorsWithoutHR();
+
+#if UNITY_2022_1_OR_NEWER
+            if (EditorSettings.spritePackerMode == SpritePackerMode.AlwaysOnAtlas || EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2) {
+                SetSuggestionsShown(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
+            } else if (CheckSuggestionActive(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022)) { 
+                SetSuggestionInactive(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
+                EditorPrefs.SetBool($"HotReloadWindow.SuggestionsShown.{HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022}", false);
+            }
+#endif
+        }
+        
+        private static void CheckEditorsWithoutHR() {
+            if (!ServerHealthCheck.I.IsServerHealthy) {
+                HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
+                return;
+            }
+            if (checkingEditorsWihtoutHR || 
+                (DateTime.UtcNow - lastCheckedUnityInstances).TotalSeconds < 5)
+            {
+                return;
+            }
+            CheckEditorsWithoutHRAsync().Forget();
+        }
+
+        static bool checkingEditorsWihtoutHR;
+        private static async Task CheckEditorsWithoutHRAsync() {
+            try {
+                checkingEditorsWihtoutHR = true;
+                var showSuggestion = await Task.Run(() => {
+                    try {
+                        var runningUnities = Process.GetProcessesByName("Unity Editor").Length;
+                        var runningPatchers = Process.GetProcessesByName("CodePatcherCLI").Length;
+                        return runningPatchers > 0 && runningUnities > runningPatchers;
+                    } catch (ArgumentException) {
+                        // On some devices GetProcessesByName throws ArgumentException for no good reason.
+                        // it happens rarely and the feature is not the most important so proper solution is not required
+                        return false;
+                    }
+                });
+                if (!showSuggestion) {
+                    HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
+                    return;
+                }
+                if (!HotReloadState.ShowedEditorsWithoutHR && ServerHealthCheck.I.IsServerHealthy) {
+                    HotReloadSuggestionsHelper.SetSuggestionActive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
+                    HotReloadState.ShowedEditorsWithoutHR = true;
+                }
+            } finally {
+                checkingEditorsWihtoutHR = false;
+                lastCheckedUnityInstances = DateTime.UtcNow;
+            }
+        }
+	}
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/HotReloadSuggestionsHelper.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 9cc471e812b143599ef5dde1d7ec022a
+timeCreated: 1694632601

+ 606 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/HotReloadTimelineHelper.cs

@@ -0,0 +1,606 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using JetBrains.Annotations;
+using SingularityGroup.HotReload.DTO;
+using SingularityGroup.HotReload.Newtonsoft.Json;
+using UnityEditor;
+using UnityEngine;
+
+
+namespace SingularityGroup.HotReload.Editor {
+    internal enum TimelineType {
+        Suggestions,
+        Timeline,
+    }
+    
+    internal enum AlertType {
+        Suggestion,
+        UnsupportedChange,
+        CompileError,
+        PartiallySupportedChange,
+        AppliedChange,
+        UndetectedChange,
+    }
+    
+    internal enum AlertEntryType {
+        Error,
+        Failure,
+        InlinedMethod,
+        PatchApplied,
+        PartiallySupportedChange,
+        UndetectedChange,
+    }
+    
+    internal enum EntryType {
+        Parent,
+        Child,
+        Standalone,
+        Foldout,
+    }
+    
+    internal class PersistedAlertData {
+        public readonly AlertData[] alertDatas;
+
+        public PersistedAlertData(AlertData[] alertDatas) {
+            this.alertDatas = alertDatas;
+        }
+    }
+
+    internal class AlertData {
+        public readonly AlertEntryType alertEntryType;
+        public readonly string errorString;
+        public readonly string methodName;
+        public readonly string methodSimpleName;
+        public readonly PartiallySupportedChange partiallySupportedChange;
+        public readonly EntryType entryType;
+        public readonly bool detiled;
+        public readonly DateTime createdAt;
+        public readonly string[] patchedMembersDisplayNames;
+
+        public AlertData(AlertEntryType alertEntryType, DateTime createdAt, bool detiled = false, EntryType entryType = EntryType.Standalone, string errorString = null, string methodName = null, string methodSimpleName = null, PartiallySupportedChange partiallySupportedChange = default(PartiallySupportedChange), string[] patchedMembersDisplayNames = null) {
+            this.alertEntryType = alertEntryType;
+            this.createdAt = createdAt;
+            this.detiled = detiled;
+            this.entryType = entryType;
+            this.errorString = errorString;
+            this.methodName = methodName;
+            this.methodSimpleName = methodSimpleName;
+            this.partiallySupportedChange = partiallySupportedChange;
+            this.patchedMembersDisplayNames = patchedMembersDisplayNames;
+        }
+    }
+    
+    internal class AlertEntry {
+        internal readonly AlertType alertType;
+        internal readonly string title;
+        internal readonly DateTime timestamp;
+        internal readonly string description;
+        [CanBeNull] internal readonly Action actionData;
+        internal readonly AlertType iconType;
+        internal readonly string shortDescription;
+        internal readonly EntryType entryType;
+        internal readonly AlertData alertData;
+        internal readonly bool hasExitButton;
+
+        internal AlertEntry(AlertType alertType, string title, string description, DateTime timestamp, string shortDescription = null, Action actionData = null, AlertType? iconType = null, EntryType entryType = EntryType.Standalone, AlertData alertData = default(AlertData), bool hasExitButton = true) {
+            this.alertType = alertType;
+            this.title = title;
+            this.description = description;
+            this.shortDescription = shortDescription;
+            this.actionData = actionData;
+            this.iconType = iconType ?? alertType;
+            this.timestamp = timestamp;
+            this.entryType = entryType;
+            this.alertData = alertData;
+            this.hasExitButton = hasExitButton;
+        }
+    }
+
+    internal static class HotReloadTimelineHelper {
+        internal const int maxVisibleEntries = 40;
+        
+        private static List<AlertEntry> eventsTimeline = new List<AlertEntry>();
+        internal static List<AlertEntry> EventsTimeline => eventsTimeline;
+
+        static readonly string filePath = Path.Combine(PackageConst.LibraryCachePath, "eventEntries.json");
+
+        public static void InitPersistedEvents() {
+            if (!File.Exists(filePath)) {
+                return;
+            }
+            var redDotShown = HotReloadState.ShowingRedDot;
+            try {
+                var persistedAlertData = JsonConvert.DeserializeObject<PersistedAlertData>(File.ReadAllText(filePath));
+                eventsTimeline = new List<AlertEntry>(persistedAlertData.alertDatas.Length);
+                for (int i = persistedAlertData.alertDatas.Length - 1; i >= 0; i--) {
+                    AlertData alertData = persistedAlertData.alertDatas[i];
+                    switch (alertData.alertEntryType) {
+                        case AlertEntryType.Error:
+                            CreateErrorEventEntry(errorString: alertData.errorString, entryType: alertData.entryType, createdAt: alertData.createdAt);
+                            break;
+#if UNITY_2020_1_OR_NEWER
+                        case AlertEntryType.InlinedMethod:
+                            CreateInlinedMethodsEntry(alertData.patchedMembersDisplayNames, alertData.entryType, alertData.createdAt);
+                            break;
+#endif
+                        case AlertEntryType.Failure:
+                            if (alertData.entryType == EntryType.Parent) {
+                                CreateReloadFinishedWithWarningsEventEntry(createdAt: alertData.createdAt, patchedMembersDisplayNames: alertData.patchedMembersDisplayNames);
+                            } else {
+                                CreatePatchFailureEventEntry(errorString: alertData.errorString, methodName: alertData.methodName, methodSimpleName: alertData.methodSimpleName, entryType: alertData.entryType, createdAt: alertData.createdAt);
+                            }
+                            break;
+                        case AlertEntryType.PatchApplied:
+                            CreateReloadFinishedEventEntry(
+                                createdAt: alertData.createdAt,
+                                patchedMethodsDisplayNames: alertData.patchedMembersDisplayNames
+                            );
+                            break;
+                        case AlertEntryType.PartiallySupportedChange:
+                            if (alertData.entryType == EntryType.Parent) {
+                                CreateReloadPartiallyAppliedEventEntry(createdAt: alertData.createdAt, patchedMethodsDisplayNames: alertData.patchedMembersDisplayNames);
+                            } else {
+                                CreatePartiallyAppliedEventEntry(alertData.partiallySupportedChange, entryType: alertData.entryType, detailed: alertData.detiled, createdAt: alertData.createdAt);
+                            }
+                            break;
+                        case AlertEntryType.UndetectedChange:
+                            CreateReloadUndetectedChangeEventEntry(createdAt: alertData.createdAt);
+                            break;
+                    }
+                }
+            } catch (Exception e) {
+                Log.Warning($"Failed initializing Hot Reload event entries on start: {e}");
+            } finally {
+                // Ensure red dot is not triggered for existing entries
+                HotReloadState.ShowingRedDot = redDotShown;
+            }
+        }
+
+        internal static void PersistTimeline() {
+            var alertDatas = new AlertData[eventsTimeline.Count];
+            for (var i = 0; i < eventsTimeline.Count; i++) {
+                alertDatas[i] = eventsTimeline[i].alertData;
+            }
+            var persistedData = new PersistedAlertData(alertDatas);
+            try {
+                File.WriteAllText(path: filePath, contents: JsonConvert.SerializeObject(persistedData));
+            } catch (Exception e) {
+                Log.Warning($"Failed persisting Hot Reload event entries: {e}");
+            }
+        }
+        
+        internal static void ClearPersistance() {
+            try {
+                File.Delete(filePath);
+            } catch {
+                // ignore
+            }
+            eventsTimeline = new List<AlertEntry>();
+        }
+        
+        internal static readonly Dictionary<AlertType, string> alertIconString = new Dictionary<AlertType, string> {
+            { AlertType.Suggestion, "alert_info" },
+            { AlertType.UnsupportedChange, "warning" },
+            { AlertType.CompileError, "error" },
+            { AlertType.PartiallySupportedChange, "infos" },
+            { AlertType.AppliedChange, "applied_patch" },
+            { AlertType.UndetectedChange, "undetected" },
+        };
+        
+#pragma warning disable CS0612 // obsolete
+        public static Dictionary<PartiallySupportedChange, string> partiallySupportedChangeDescriptions = new Dictionary<PartiallySupportedChange, string> {
+            {PartiallySupportedChange.LambdaClosure, "A lambda closure was edited (captured variable was added or removed). Changes to it will only be visible to the next created lambda(s)."},
+            {PartiallySupportedChange.EditAsyncMethod, "An async method was edited. Changes to it will only be visible the next time this method is called."},
+            {PartiallySupportedChange.AddMonobehaviourMethod, "A new method was added. It will not show up in the Inspector until the next full recompilation."},
+            {PartiallySupportedChange.EditMonobehaviourField, "A field in a MonoBehaviour was removed or reordered. The inspector will not notice this change until the next full recompilation."},
+            {PartiallySupportedChange.EditCoroutine, "An IEnumerator/IEnumerable was edited. When used as a coroutine, changes to it will only be visible the next time the coroutine is created."},
+            {PartiallySupportedChange.EditGenericFieldInitializer, "A field initializer inside generic class was edited. Field initializer will not have any effect until the next full recompilation."},
+            {PartiallySupportedChange.AddEnumMember, "An enum member was added. ToString and other reflection methods work only after the next full recompilation. Additionally, changes to the enum order may not apply until you patch usages in other places of the code."},
+            {PartiallySupportedChange.EditFieldInitializer, "A field initializer was edited. Changes will only apply to new instances of that type, since the initializer for an object only runs when it is created."},
+            {PartiallySupportedChange.AddMethodWithAttributes, "A method with attributes was added. Method attributes will not have any effect until the next full recompilation."},
+            {PartiallySupportedChange.AddFieldWithAttributes, "A field with attributes was added. Field attributes will not have any effect until the next full recompilation."},
+            {PartiallySupportedChange.GenericMethodInGenericClass, "A generic method was edited. Usages in non-generic classes applied, but usages in the generic classes are not supported."},
+            {PartiallySupportedChange.NewCustomSerializableField, "A new custom serializable field was added. The inspector will not notice this change until the next full recompilation."},
+            {PartiallySupportedChange.MultipleFieldsEditedInTheSameType, "Multiple fields modified in the same type during a single patch. Their values have been reset."},
+        };
+#pragma warning restore CS0612
+        
+        internal static List<AlertEntry> Suggestions = new List<AlertEntry>();
+        internal static int UnsupportedChangesCount => EventsTimeline.Count(alert => alert.alertType == AlertType.UnsupportedChange && alert.entryType != EntryType.Child);
+        internal static int PartiallySupportedChangesCount => EventsTimeline.Count(alert => alert.alertType == AlertType.PartiallySupportedChange && alert.entryType != EntryType.Child);
+        internal static int UndetectedChangesCount => EventsTimeline.Count(alert => alert.alertType == AlertType.UndetectedChange && alert.entryType != EntryType.Child);
+        internal static int CompileErrorsCount => EventsTimeline.Count(alert => alert.alertType == AlertType.CompileError);
+        internal static int AppliedChangesCount => EventsTimeline.Count(alert => alert.alertType == AlertType.AppliedChange);
+
+        static Regex shortDescriptionRegex = new Regex(@"^(\w+)\s(\w+)(?=:)", RegexOptions.Compiled);
+        
+        internal static int GetRunTabTimelineEventCount() {
+            int total = 0;
+            if (HotReloadPrefs.RunTabUnsupportedChangesFilter) {
+                total += UnsupportedChangesCount;
+            }
+            if (HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter) {
+                total += PartiallySupportedChangesCount;
+            }
+            if (HotReloadPrefs.RunTabUndetectedPatchesFilter) {
+                total += UndetectedChangesCount;
+            }
+            if (HotReloadPrefs.RunTabCompileErrorFilter) {
+                total += CompileErrorsCount;
+            }
+            if (HotReloadPrefs.RunTabAppliedPatchesFilter) {
+                total += AppliedChangesCount;
+            }
+            return total;
+        }
+        
+        internal static List<AlertEntry> expandedEntries = new List<AlertEntry>();
+        
+        internal static void RenderCompileButton() {
+            if (GUILayout.Button("Recompile", GUILayout.Width(80))) {
+                HotReloadRunTab.RecompileWithChecks();
+            }
+        }
+        
+        private static float maxScrollPos;
+        internal static void RenderErrorEventActions(string description, ErrorData errorData) {
+            int maxLen = 2400;
+            string text = errorData.stacktrace;
+            if (text.Length > maxLen) {
+                text = text.Substring(0, maxLen) + "...";
+            }
+
+            GUILayout.TextArea(text, HotReloadWindowStyles.StacktraceTextAreaStyle);
+
+            if (errorData.file || !errorData.stacktrace.Contains("error CS")) {
+                GUILayout.Space(10f);
+            }
+            
+            using (new EditorGUILayout.HorizontalScope()) {
+                if (!errorData.stacktrace.Contains("error CS")) {
+                    RenderCompileButton();
+                }
+            
+                // Link
+                if (errorData.file) {
+                    GUILayout.FlexibleSpace();
+                    if (GUILayout.Button(errorData.linkString, HotReloadWindowStyles.LinkStyle)) {
+                        AssetDatabase.OpenAsset(errorData.file, Math.Max(errorData.lineNumber, 1));
+                    }
+                }
+            }
+        }
+
+        private static Texture2D GetFilterIcon(int count, AlertType alertType) {
+            if (count == 0) {
+                return GUIHelper.ConvertToGrayscale(alertIconString[alertType]);
+            }
+            return GUIHelper.GetLocalIcon(alertIconString[alertType]);
+        }
+        
+        internal static void RenderAlertFilters() {
+            using (new EditorGUILayout.HorizontalScope()) {
+                var text = AppliedChangesCount > 999 ? "999+" : " " + AppliedChangesCount;
+                
+                HotReloadPrefs.RunTabAppliedPatchesFilter = GUILayout.Toggle(
+                    HotReloadPrefs.RunTabAppliedPatchesFilter,
+                    new GUIContent(text, GetFilterIcon(AppliedChangesCount, AlertType.AppliedChange)), 
+                    HotReloadWindowStyles.EventFiltersStyle);
+                
+                GUILayout.Space(-1f);
+                
+                text = UndetectedChangesCount > 999 ? "999+" : " " + UndetectedChangesCount;
+                HotReloadPrefs.RunTabUndetectedPatchesFilter = GUILayout.Toggle(
+                    HotReloadPrefs.RunTabUndetectedPatchesFilter,
+                    new GUIContent(text, GetFilterIcon(UnsupportedChangesCount, AlertType.UndetectedChange)), 
+                    HotReloadWindowStyles.EventFiltersStyle);
+                
+                GUILayout.Space(-1f);
+                
+                text = PartiallySupportedChangesCount > 999 ? "999+" : " " + PartiallySupportedChangesCount;
+                HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter = GUILayout.Toggle(
+                    HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter,
+                    new GUIContent(text, GetFilterIcon(PartiallySupportedChangesCount, AlertType.PartiallySupportedChange)), 
+                    HotReloadWindowStyles.EventFiltersStyle);
+                
+                GUILayout.Space(-1f);
+                
+                text = UnsupportedChangesCount > 999 ? "999+" : " " + UnsupportedChangesCount;
+                HotReloadPrefs.RunTabUnsupportedChangesFilter = GUILayout.Toggle(
+                    HotReloadPrefs.RunTabUnsupportedChangesFilter, 
+                    new GUIContent(text, GetFilterIcon(UnsupportedChangesCount, AlertType.UnsupportedChange)), 
+                    HotReloadWindowStyles.EventFiltersStyle);
+                
+                GUILayout.Space(-1f);
+                
+                text = CompileErrorsCount > 999 ? "999+" : " " + CompileErrorsCount;
+                HotReloadPrefs.RunTabCompileErrorFilter = GUILayout.Toggle(
+                    HotReloadPrefs.RunTabCompileErrorFilter,
+                    new GUIContent(text, GetFilterIcon(CompileErrorsCount, AlertType.CompileError)), 
+                    HotReloadWindowStyles.EventFiltersStyle);
+            }
+        }
+
+        internal static void CreateErrorEventEntry(string errorString, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
+            var timestamp = createdAt ?? DateTime.Now;
+            var alertType = errorString.Contains("error CS")
+                ? AlertType.CompileError
+                : AlertType.UnsupportedChange;
+            var title = errorString.Contains("error CS")
+                ? "Compile error"
+                : "Unsupported change";
+            ErrorData errorData = ErrorData.GetErrorData(errorString);
+            var description = errorData.error;
+            string shortDescription = null;
+            if (alertType != AlertType.CompileError) {
+                shortDescription = shortDescriptionRegex.Match(description).Value;
+            }
+            Action actionData = () => RenderErrorEventActions(description, errorData);
+            InsertEntry(new AlertEntry(
+                timestamp: timestamp,
+                alertType: alertType, 
+                title: title, 
+                description: description, 
+                shortDescription: shortDescription, 
+                actionData: actionData,
+                entryType: entryType,
+                alertData: new AlertData(AlertEntryType.Error, createdAt: timestamp, errorString: errorString, entryType: entryType)
+            ));
+        }
+        
+#if UNITY_2020_1_OR_NEWER
+        internal static void CreateInlinedMethodsEntry(string[] patchedMethodsDisplayNames, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
+            var truncated = false;
+            if (patchedMethodsDisplayNames?.Length > 25) {
+                patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
+                truncated = true;
+            }
+            var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
+            var timestamp = createdAt ?? DateTime.Now;
+            var entry = new AlertEntry(
+                timestamp: timestamp,
+                alertType : AlertType.UnsupportedChange, 
+                title: "Failed applying patch to method", 
+                description: $"Some methods got inlined by the Unity compiler and cannot be patched by Hot Reload. Switch to Debug mode to avoid this problem.\n\n• {(truncated ? patchesList + "\n..." : patchesList)}",
+                entryType: EntryType.Parent,
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        RenderCompileButton();
+                        var suggestion = HotReloadSuggestionsHelper.suggestionMap[HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods];
+                        if (suggestion?.actionData != null) {
+                            suggestion.actionData();
+                        }
+                    }
+                },
+                alertData: new AlertData(AlertEntryType.InlinedMethod, createdAt: timestamp, patchedMembersDisplayNames: patchedMethodsDisplayNames, entryType: EntryType.Parent)
+            );
+            InsertEntry(entry);
+            if (patchedMethodsDisplayNames?.Length > 0) {
+                expandedEntries.Add(entry);
+            }
+        }
+#endif
+        
+        internal static void CreatePatchFailureEventEntry(string errorString, string methodName, string methodSimpleName = null, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
+            var timestamp = createdAt ?? DateTime.Now;
+            ErrorData errorData = ErrorData.GetErrorData(errorString);
+            var title = $"Failed applying patch to method";
+            Action actionData = () => RenderErrorEventActions(errorData.error, errorData);
+            InsertEntry(new AlertEntry(
+                timestamp: timestamp,
+                alertType : AlertType.UnsupportedChange, 
+                title: title, 
+                description: $"{title}: {methodName}, tap here to see more.",
+                shortDescription: methodSimpleName, 
+                actionData: actionData,
+                entryType: entryType,
+                alertData: new AlertData(AlertEntryType.Failure, createdAt: timestamp, errorString: errorString, methodName: methodName, methodSimpleName: methodSimpleName, entryType: entryType)
+            ));
+        }
+
+        public static T[] TruncateList<T>(T[] originalList, int len) {
+            if (originalList.Length <= len) {
+                return originalList;
+            }
+            // Create a new list with a maximum of 25 items
+            T[] truncatedList = new T[len];
+
+            for (int i = 0; i < originalList.Length && i < len; i++) {
+                truncatedList[i] = originalList[i];
+            }
+
+            return truncatedList;
+        }
+        
+        internal static void CreateReloadFinishedEventEntry(DateTime? createdAt = null, string[] patchedMethodsDisplayNames = null) {
+            var truncated = false;
+            if (patchedMethodsDisplayNames?.Length > 25) {
+                patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
+                truncated = true;
+            }
+            var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
+            var timestamp = createdAt ?? DateTime.Now;
+            var entry = new AlertEntry(
+                timestamp: timestamp,
+                alertType: AlertType.AppliedChange,
+                title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Reloaded],
+                description: patchedMethodsDisplayNames?.Length > 0 
+                    ? $"• {(truncated ? patchesList + "\n..." : patchesList)}" 
+                    : "No issues found",
+                entryType: patchedMethodsDisplayNames?.Length > 0 ? EntryType.Parent : EntryType.Standalone,
+                alertData: new AlertData(
+                    AlertEntryType.PatchApplied, 
+                    createdAt: timestamp, 
+                    entryType: EntryType.Standalone,
+                    patchedMembersDisplayNames: patchedMethodsDisplayNames)
+            );
+            
+            InsertEntry(entry);
+            if (patchedMethodsDisplayNames?.Length > 0) {
+                expandedEntries.Add(entry);
+            }
+        }
+        
+        internal static void CreateReloadFinishedWithWarningsEventEntry(DateTime? createdAt = null, string[] patchedMembersDisplayNames = null) {
+            var truncated = false;
+            if (patchedMembersDisplayNames?.Length > 25) {
+                patchedMembersDisplayNames = TruncateList(patchedMembersDisplayNames, 25);
+                truncated = true;
+            }
+            var patchesList = patchedMembersDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMembersDisplayNames) : "";
+            var timestamp = createdAt ?? DateTime.Now;
+            var entry = new AlertEntry(
+                timestamp: timestamp,
+                alertType: AlertType.UnsupportedChange,
+                title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Unsupported],
+                description: patchedMembersDisplayNames?.Length > 0 ? $"• {(truncated ? patchesList + "\n...\n\nSee unsupported changes below" : patchesList + "\n\nSee unsupported changes below")}" : "See detailed entries below",
+                entryType: EntryType.Parent,
+                alertData: new AlertData(AlertEntryType.Failure, createdAt: timestamp, entryType: EntryType.Parent, patchedMembersDisplayNames: patchedMembersDisplayNames)
+            );
+            InsertEntry(entry);
+            if (patchedMembersDisplayNames?.Length > 0) {
+                expandedEntries.Add(entry);
+            }
+        }
+        
+        internal static void CreateReloadPartiallyAppliedEventEntry(DateTime? createdAt = null, string[] patchedMethodsDisplayNames = null) {
+            var truncated = false;
+            if (patchedMethodsDisplayNames?.Length > 25) {
+                patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
+                truncated = true;
+            }
+            var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
+            var timestamp = createdAt ?? DateTime.Now;
+            var entry = new AlertEntry(
+                timestamp: timestamp,
+                alertType: AlertType.PartiallySupportedChange,
+                title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.PartiallySupported],
+                description: patchedMethodsDisplayNames?.Length > 0 ? $"• {(truncated ? patchesList + "\n...\n\nSee partially applied changes below" : patchesList + "\n\nSee partially applied changes below")}"  : "See detailed entries below",
+                entryType: EntryType.Parent,
+                alertData: new AlertData(AlertEntryType.PartiallySupportedChange, createdAt: timestamp, entryType: EntryType.Parent, patchedMembersDisplayNames: patchedMethodsDisplayNames)
+            );
+            InsertEntry(entry);
+            if (patchedMethodsDisplayNames?.Length > 0) {
+                expandedEntries.Add(entry);
+            }
+        }
+        
+        internal static void CreateReloadUndetectedChangeEventEntry(DateTime? createdAt = null) {
+            var timestamp = createdAt ?? DateTime.Now;
+            InsertEntry(new AlertEntry(
+                timestamp: timestamp,
+                alertType : AlertType.UndetectedChange, 
+                title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Undetected],
+                description: "Code semantics didn't change (e.g. whitespace) or the change requires manual recompile.\n\n" +
+                           "Recompile to force-apply changes.",
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        RenderCompileButton();
+                        GUILayout.FlexibleSpace();
+                        OpenURLButton.Render("Docs", Constants.UndetectedChangesURL);
+                        GUILayout.Space(10f);
+                    }
+                },
+                entryType: EntryType.Foldout,
+                alertData: new AlertData(AlertEntryType.UndetectedChange, createdAt: timestamp, entryType: EntryType.Parent)
+            ));
+        }
+        
+        internal static void CreatePartiallyAppliedEventEntry(PartiallySupportedChange partiallySupportedChange, EntryType entryType = EntryType.Standalone, bool detailed = true, DateTime? createdAt = null) {
+            var timestamp = createdAt ?? DateTime.Now;
+            string description;
+            if (!partiallySupportedChangeDescriptions.TryGetValue(partiallySupportedChange, out description)) {
+                return;
+            }
+            InsertEntry(new AlertEntry(
+                timestamp: timestamp,
+                alertType : AlertType.PartiallySupportedChange, 
+                title : detailed ? "Change partially applied" : ToString(partiallySupportedChange),
+                description : description,
+                shortDescription: detailed ? ToString(partiallySupportedChange) : null,
+                actionData: () => {
+                    GUILayout.Space(10f);
+                    using (new EditorGUILayout.HorizontalScope()) {
+                        RenderCompileButton();
+                        GUILayout.FlexibleSpace();
+                        if (GetPartiallySupportedChangePref(partiallySupportedChange)) {
+                            if (GUILayout.Button("Ignore this event type ", HotReloadWindowStyles.LinkStyle)) {
+                                HidePartiallySupportedChange(partiallySupportedChange);
+                                HotReloadRunTab.RepaintInstant();
+                            }
+                        }
+                    }
+                },
+                entryType: entryType,
+                alertData: new AlertData(AlertEntryType.PartiallySupportedChange, createdAt: timestamp, partiallySupportedChange: partiallySupportedChange, entryType: entryType, detiled: detailed)
+            ));
+        }
+        
+        internal static void InsertEntry(AlertEntry entry) {
+            eventsTimeline.Insert(0, entry);
+            if (entry.alertType != AlertType.AppliedChange) {
+                HotReloadState.ShowingRedDot = true;
+            }
+        }
+        
+        internal static void ClearEntries() {
+            eventsTimeline.Clear();
+        }
+        
+        internal static bool GetPartiallySupportedChangePref(PartiallySupportedChange key) {
+            return EditorPrefs.GetBool($"HotReloadWindow.ShowPartiallySupportedChangeType.{key}", true);
+        }
+        
+        internal static void HidePartiallySupportedChange(PartiallySupportedChange key) {
+            EditorPrefs.SetBool($"HotReloadWindow.ShowPartiallySupportedChangeType.{key}", false);
+            // loop over scroll entries to remove hidden entries
+            for (var i = EventsTimeline.Count - 1; i >= 0; i--) {
+                var eventEntry = EventsTimeline[i];
+                if (eventEntry.alertData.partiallySupportedChange == key) {
+                    EventsTimeline.Remove(eventEntry);
+                }
+            }
+        }
+
+        // performance optimization (Enum.ToString uses reflection)
+        internal static string ToString(this PartiallySupportedChange change) {
+#pragma warning disable CS0612 // obsolete
+            switch (change) {
+                case PartiallySupportedChange.LambdaClosure:
+                    return nameof(PartiallySupportedChange.LambdaClosure);
+                case PartiallySupportedChange.EditAsyncMethod:
+                   return nameof(PartiallySupportedChange.EditAsyncMethod);
+                case PartiallySupportedChange.AddMonobehaviourMethod:
+                   return nameof(PartiallySupportedChange.AddMonobehaviourMethod);
+                case PartiallySupportedChange.EditMonobehaviourField:
+                    return nameof(PartiallySupportedChange.EditMonobehaviourField);
+                case PartiallySupportedChange.EditCoroutine:
+                   return nameof(PartiallySupportedChange.EditCoroutine);
+                case PartiallySupportedChange.EditGenericFieldInitializer:
+                   return nameof(PartiallySupportedChange.EditGenericFieldInitializer);
+                case PartiallySupportedChange.AddEnumMember:
+                   return nameof(PartiallySupportedChange.AddEnumMember);
+                case PartiallySupportedChange.EditFieldInitializer:
+                   return nameof(PartiallySupportedChange.EditFieldInitializer);
+                case PartiallySupportedChange.AddMethodWithAttributes:
+                   return nameof(PartiallySupportedChange.AddMethodWithAttributes);
+                case PartiallySupportedChange.GenericMethodInGenericClass:
+                   return nameof(PartiallySupportedChange.GenericMethodInGenericClass);
+                case PartiallySupportedChange.AddFieldWithAttributes:
+                   return nameof(PartiallySupportedChange.AddFieldWithAttributes);
+                case PartiallySupportedChange.NewCustomSerializableField:
+                   return nameof(PartiallySupportedChange.NewCustomSerializableField);
+                case PartiallySupportedChange.MultipleFieldsEditedInTheSameType:
+                   return nameof(PartiallySupportedChange.MultipleFieldsEditedInTheSameType);
+#pragma warning restore CS0612
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(change), change, null);
+            }
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/HotReloadTimelineHelper.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: ffb65be71b8b4d14800f8b28bf68d0ab
+timeCreated: 1695210350

+ 80 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/Spinner.cs

@@ -0,0 +1,80 @@
+using System;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+    internal class Spinner {
+        internal static string SpinnerIconPath => "icon_loading_star_light_mode_96";
+        internal static Texture2D spinnerTexture => GUIHelper.GetInvertibleIcon(InvertibleIcon.Spinner);
+        private Texture2D _rotatedTextureLight;
+        private Texture2D _rotatedTextureDark;
+        private Texture2D rotatedTextureLight => _rotatedTextureLight ? _rotatedTextureLight : _rotatedTextureLight = GetCopy(spinnerTexture);
+        private Texture2D rotatedTextureDark => _rotatedTextureDark ? _rotatedTextureDark : _rotatedTextureDark = GetCopy(spinnerTexture);
+        internal Texture2D rotatedTexture => HotReloadWindowStyles.IsDarkMode ? rotatedTextureDark : rotatedTextureLight;
+        
+        private float _rotationAngle;
+        private DateTime _lastRotation;
+        private int _rotationPeriod;
+        
+        internal Spinner(int rotationPeriodInMilliseconds) {
+            _rotationPeriod = rotationPeriodInMilliseconds;
+        }
+        
+        internal Texture2D GetIcon() {
+            if (DateTime.UtcNow - _lastRotation > TimeSpan.FromMilliseconds(_rotationPeriod)) {
+                _lastRotation = DateTime.UtcNow;
+                _rotationAngle += 45;
+                if (_rotationAngle >= 360f) 
+                    _rotationAngle -= 360f;
+                return RotateImage(spinnerTexture, _rotationAngle);
+            }
+            return rotatedTexture;
+        }
+        
+        private Texture2D RotateImage(Texture2D originalTexture, float angle) {
+            int w = originalTexture.width;
+            int h = originalTexture.height;
+            
+            int x, y;
+            float centerX = w / 2f;
+            float centerY = h / 2f;
+
+            for (x = 0; x < w; x++) {
+                for (y = 0; y < h; y++) {
+                    float dx = x - centerX;
+                    float dy = y - centerY;
+                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
+                    float oldAngle = Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
+                    float newAngle = oldAngle + angle;
+
+                    float newX = centerX + distance * Mathf.Cos(newAngle * Mathf.Deg2Rad);
+                    float newY = centerY + distance * Mathf.Sin(newAngle * Mathf.Deg2Rad);
+
+                    if (newX >= 0 && newX < w && newY >= 0 && newY < h) {
+                        rotatedTexture.SetPixel(x, y, originalTexture.GetPixel((int)newX, (int)newY));
+                    } else {
+                        rotatedTexture.SetPixel(x, y, Color.clear);
+                    }
+                }
+            }
+
+            rotatedTexture.Apply();
+            return rotatedTexture;
+        }
+
+        public static Texture2D GetCopy(Texture2D tex, TextureFormat format = TextureFormat.RGBA32, bool mipChain = false) {
+            var tmp = RenderTexture.GetTemporary(tex.width, tex.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear);
+            Graphics.Blit(tex, tmp);
+
+            RenderTexture.active = tmp;
+            try {
+                var copy = new Texture2D(tex.width, tex.height, format, mipChain: mipChain);
+                copy.ReadPixels(new Rect(0, 0, tmp.width, tmp.height), 0, 0);
+                copy.Apply();
+                return copy;
+            } finally {
+                RenderTexture.active = null;
+                RenderTexture.ReleaseTemporary(tmp);
+            }
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/Spinner.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 8bd77f0465824c5da3e1454f75c6e93c
+timeCreated: 1685871830

+ 95 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/UnitySettingsHelper.cs

@@ -0,0 +1,95 @@
+using UnityEngine;
+using System.Reflection;
+using System;
+using System.Collections;
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("SingularityGroup.HotReload.Demo")]
+
+namespace SingularityGroup.HotReload.Editor {
+    internal class UnitySettingsHelper {
+        public static UnitySettingsHelper I = new UnitySettingsHelper();
+
+        private bool initialized;
+        private object pref;
+        private PropertyInfo prefColorProp;
+        private MethodInfo setMethod;
+        private Type settingsType;
+        private Type prefColorType;
+        const string currentPlaymodeTintPrefKey = "Playmode tint";
+
+        internal bool playmodeTintSupported => EditorCodePatcher.config.changePlaymodeTint && EnsureInitialized();
+
+        private UnitySettingsHelper() {
+            EnsureInitialized();
+        }
+        
+
+        private bool EnsureInitialized() {
+            if (initialized) {
+                return true;
+            }
+            try {
+                // cache members for performance
+                settingsType = settingsType ?? (settingsType = typeof(UnityEditor.Editor).Assembly.GetType($"UnityEditor.PrefSettings"));
+                prefColorType = prefColorType ?? (prefColorType = typeof(UnityEditor.Editor).Assembly.GetType($"UnityEditor.PrefColor"));
+                prefColorProp = prefColorProp ?? (prefColorProp = prefColorType?.GetProperty("Color", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public));
+                pref = pref ?? (pref = GetPref(settingsType: settingsType, prefColorType: prefColorType));
+                setMethod = setMethod ?? (setMethod = GetSetMethod(settingsType: settingsType, prefColorType: prefColorType));
+
+                if (prefColorProp == null
+                    || pref == null
+                    || setMethod == null
+                ) {
+                    return false;
+                }
+                
+                // clear cache for performance
+                settingsType = null;
+                prefColorType = null;
+
+                initialized = true;
+                return true;
+            } catch {
+                return false;
+            }
+        }
+
+        private static MethodInfo GetSetMethod(Type settingsType, Type prefColorType) {
+            var setMethodBase = settingsType?.GetMethod("Set", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
+            return setMethodBase?.MakeGenericMethod(prefColorType);
+        }
+
+        private static object GetPref(Type settingsType, Type prefColorType) {
+            var prefsMethodBase = settingsType?.GetMethod("Prefs", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
+            var prefsMethod = prefsMethodBase?.MakeGenericMethod(prefColorType);
+            var prefs = (IEnumerable)prefsMethod?.Invoke(null, Array.Empty<object>());
+            if (prefs != null) {
+                foreach (object kvp in prefs) {
+                    var key = kvp.GetType().GetProperty("Key", BindingFlags.Instance | BindingFlags.Public)?.GetMethod.Invoke(kvp, Array.Empty<object>());
+                    if (key?.ToString() == currentPlaymodeTintPrefKey) {
+                        return kvp.GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public)?.GetMethod.Invoke(kvp, Array.Empty<object>());
+                    }
+
+                }
+            }
+            return null;
+        }
+
+        public Color? GetCurrentPlaymodeColor() {
+            if (!playmodeTintSupported) {
+                return null;
+            }
+            return (Color)prefColorProp.GetValue(pref);
+        }
+        
+        public void SetPlaymodeTint(Color color) {
+            if (!playmodeTintSupported) {
+                return;
+            }
+            prefColorProp.SetValue(pref, color);
+            setMethod.Invoke(null, new object[] { currentPlaymodeTintPrefKey, pref });
+        }
+    }
+}
+

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Helpers/UnitySettingsHelper.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 34fb1222dc00466ab4e3db7383bd00ee
+timeCreated: 1694279476

+ 35 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadAttributeProcessor.cs

@@ -0,0 +1,35 @@
+#if ODIN_INSPECTOR
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using SingularityGroup.HotReload.EditorDependencies;
+using Sirenix.OdinInspector;
+using Sirenix.OdinInspector.Editor;
+
+namespace SingularityGroup.HotReload.Editor {
+	public class HotReloadAttributeProcessor : OdinAttributeProcessor {
+		public override bool CanProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member) {
+			return member is FieldInfo;
+		}
+
+		static object nullObject = new object();
+		public override void ProcessChildMemberAttributes(InspectorProperty property, MemberInfo member, List<Attribute> attributes) {
+			var field = member as FieldInfo;
+			if (field?.DeclaringType == null) {
+				return;
+			}
+			if (UnityFieldHelper.TryGetFieldAttributes(field, out var fieldAttributes)) {
+				attributes.Clear();
+				attributes.AddRange(fieldAttributes);
+			}
+			if (UnityFieldHelper.IsFieldHidden(field.DeclaringType, field.Name)) {
+				attributes.Add(new HideIfAttribute("@true"));
+			}
+			// we assume this is always not null. Most of the times it will not be. If it is the side effect is some memory footprint which hopefully gets cleared when enough objects
+			var key = property.ParentValues.FirstOrDefault() ?? nullObject;
+			UnityFieldHelper.CacheFieldInvalidation(key, field, property.RefreshSetup);
+		}
+	}
+}
+#endif

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadAttributeProcessor.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c63452dd912fe4c46909c1c5ce844e69
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 95 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadEventPopup.cs

@@ -0,0 +1,95 @@
+using UnityEngine;
+using UnityEditor;
+
+
+namespace SingularityGroup.HotReload.Editor {
+    public enum PopupSource {
+        Window,
+        Overlay,
+    }
+    public class HotReloadEventPopup : PopupWindowContent {
+        public static HotReloadEventPopup I = new HotReloadEventPopup();
+        private Vector2 _PopupScrollPos;
+        public bool open { get; private set; }
+        private PopupSource source;
+        private HotReloadRunTabState currentState;
+        
+        public static void Open(PopupSource source, Vector2 pos) {
+            I.source = source;
+            PopupWindow.Show(new Rect(pos.x, pos.y, 0, 0), I);
+        }
+        
+        public override Vector2 GetWindowSize() {
+            if (HotReloadRunTab.ShouldRenderConsumption(currentState)
+                && (HotReloadWindowStyles.windowScreenWidth <= Constants.ConsumptionsHideWidth
+                || HotReloadWindowStyles.windowScreenHeight <= Constants.ConsumptionsHideHeight
+                || source == PopupSource.Overlay)
+            ) {
+                return new Vector2(600, 450);
+            } else {
+                return new Vector2(500, 375);
+            }
+        }
+        
+        public void Repaint() {
+            if (open) {
+                PopupWindow.GetWindow<PopupWindow>().Repaint();
+            }
+        }
+
+        public override void OnGUI(Rect rect) {
+            if (Event.current.type == EventType.Layout) {
+                currentState = HotReloadRunTabState.Current;
+            }
+            if (HotReloadWindowStyles.windowScreenWidth <= Constants.UpgradeLicenseNoteHideWidth
+                || HotReloadWindowStyles.windowScreenHeight <= Constants.UpgradeLicenseNoteHideHeight
+                || source == PopupSource.Overlay
+            ) {
+                HotReloadRunTab.RenderUpgradeLicenseNote(currentState, HotReloadWindowStyles.UpgradeLicenseButtonOverlayStyle);
+            }
+            using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) {
+                using (var scope = new EditorGUILayout.ScrollViewScope(_PopupScrollPos, GUIStyle.none, GUI.skin.verticalScrollbar, GUILayout.MaxHeight(495))) {
+                    _PopupScrollPos.x = scope.scrollPosition.x;
+                    _PopupScrollPos.y = scope.scrollPosition.y;
+
+                    if (HotReloadWindowStyles.windowScreenWidth <= Constants.ConsumptionsHideWidth
+                        || HotReloadWindowStyles.windowScreenHeight <= Constants.ConsumptionsHideHeight
+                        || source == PopupSource.Overlay
+                    ) {
+                        HotReloadRunTab.RenderLicenseInfo(currentState);
+                    }
+
+                    HotReloadRunTab.RenderBars(currentState);
+                }
+            }
+
+            bool rateAppShown = HotReloadWindow.ShouldShowRateApp();
+            if ((HotReloadWindowStyles.windowScreenWidth <= Constants.RateAppHideWidth
+                || HotReloadWindowStyles.windowScreenHeight <= Constants.RateAppHideHeight
+                || source == PopupSource.Overlay)
+                && rateAppShown
+            ) {
+                HotReloadWindow.RenderRateApp();
+            }
+            
+            if (HotReloadWindowStyles.windowScreenWidth <= Constants.EventFiltersShownHideWidth
+                || source == PopupSource.Overlay
+            ) {
+                using (new EditorGUILayout.HorizontalScope()) {
+                    GUILayout.Space(21);
+                    HotReloadTimelineHelper.RenderAlertFilters();
+                }
+            }
+            HotReloadState.ShowingRedDot = false;
+        }
+        
+        public override void OnOpen() {
+            open = true;
+        }
+        
+        public override void OnClose() {
+            _PopupScrollPos = Vector2.zero;
+            open = false;
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadEventPopup.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 00ec214cde074cf298acef73bb09a4fc
+timeCreated: 1696574416

+ 178 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadOverlay.cs

@@ -0,0 +1,178 @@
+#if UNITY_2021_2_OR_NEWER
+using System;
+using System.Collections.Generic;
+using UnityEditor.Overlays;
+using UnityEngine.UIElements;
+using UnityEditor;
+using UnityEngine;
+using UnityEditor.Toolbars;
+
+namespace SingularityGroup.HotReload.Editor {
+    [Overlay(typeof(SceneView), "Hot Reload", true)]
+    [Icon("Assets/HotReload/Editor/Resources/Icon_DarkMode.png")]
+    internal class HotReloadOverlay : ToolbarOverlay {
+        HotReloadOverlay() : base(HotReloadToolbarIndicationButton.id, HotReloadToolbarEventsButton.id, HotReloadToolbarRecompileButton.id) {
+            EditorApplication.update += Update;
+        }
+        
+        EditorIndicationState.IndicationStatus lastIndicationStatus;
+        
+        [EditorToolbarElement(id, typeof(SceneView))]
+        class HotReloadToolbarIndicationButton : EditorToolbarButton, IAccessContainerWindow {
+            internal const string id = "HotReloadOverlay/LogoButton";
+            public EditorWindow containerWindow { get; set; }
+
+            EditorIndicationState.IndicationStatus lastIndicationStatus;
+            
+            internal HotReloadToolbarIndicationButton() {
+                icon = GetIndicationIcon();
+                tooltip = EditorIndicationState.IndicationStatusText;
+                clicked += OnClick;
+                EditorApplication.update += Update;
+            }
+
+            void OnClick() {
+                EditorWindow.GetWindow<HotReloadWindow>().Show();
+                EditorWindow.GetWindow<HotReloadWindow>().SelectTab(typeof(HotReloadRunTab));
+            }
+       
+            void Update() {
+                if (lastIndicationStatus != EditorIndicationState.CurrentIndicationStatus) {
+                    icon = GetIndicationIcon();
+                    tooltip = EditorIndicationState.IndicationStatusText;
+                    lastIndicationStatus = EditorIndicationState.CurrentIndicationStatus;
+                }
+            }
+
+            ~HotReloadToolbarIndicationButton() {
+                clicked -= OnClick;
+                EditorApplication.update -= Update;
+            }
+        }
+        
+        [EditorToolbarElement(id, typeof(SceneView))]
+        class HotReloadToolbarEventsButton : EditorToolbarButton, IAccessContainerWindow {
+            internal const string id = "HotReloadOverlay/EventsButton";
+            public EditorWindow containerWindow { get; set; }
+            
+            bool lastShowingRedDot;
+            
+            internal HotReloadToolbarEventsButton() {
+                icon = HotReloadState.ShowingRedDot ? GUIHelper.GetInvertibleIcon(InvertibleIcon.EventsNew) : GUIHelper.GetInvertibleIcon(InvertibleIcon.Events);
+                tooltip = "Events";
+                clicked += OnClick;
+                EditorApplication.update += Update;
+            }
+
+            void OnClick() {
+                HotReloadEventPopup.Open(PopupSource.Overlay, Event.current.mousePosition);
+            }
+       
+            void Update() {
+                if (lastShowingRedDot != HotReloadState.ShowingRedDot) {
+                    icon = HotReloadState.ShowingRedDot ? GUIHelper.GetInvertibleIcon(InvertibleIcon.EventsNew) : GUIHelper.GetInvertibleIcon(InvertibleIcon.Events);
+                    lastShowingRedDot = HotReloadState.ShowingRedDot;
+                }
+            }
+
+            ~HotReloadToolbarEventsButton() {
+                clicked -= OnClick;
+                EditorApplication.update -= Update;
+            }
+        }
+        
+        
+        [EditorToolbarElement(id, typeof(SceneView))]
+        class HotReloadToolbarRecompileButton : EditorToolbarButton, IAccessContainerWindow {
+            internal const string id = "HotReloadOverlay/RecompileButton";
+            
+            public EditorWindow containerWindow { get; set; }
+            
+            private Texture2D refreshIcon => GUIHelper.GetInvertibleIcon(InvertibleIcon.Recompile);
+            internal HotReloadToolbarRecompileButton() {
+                icon = refreshIcon;
+                tooltip = "Recompile";
+                clicked += HotReloadRunTab.RecompileWithChecks;
+            }
+        }
+
+        private static Texture2D latestIcon;
+        private static Dictionary<string, Texture2D> iconTextures = new Dictionary<string, Texture2D>();
+        private static Spinner spinner = new Spinner(100);
+        private static Texture2D GetIndicationIcon() {
+            if (EditorIndicationState.IndicationIconPath == null || EditorIndicationState.SpinnerActive) {
+                latestIcon = spinner.GetIcon();
+            } else {
+                latestIcon = GUIHelper.GetLocalIcon(EditorIndicationState.IndicationIconPath);
+            }
+            return latestIcon;
+        }
+
+        private static Image indicationIcon;
+        private static Label indicationText;
+
+        bool initialized;
+        /// <summary>
+        /// Create Hot Reload overlay panel.
+        /// </summary>
+        public override VisualElement CreatePanelContent() {
+            var root = new VisualElement() { name = "Hot Reload Indication" };
+            root.style.flexDirection = FlexDirection.Row;
+            
+            indicationIcon = new Image() { image = GUIHelper.GetLocalIcon(EditorIndicationState.greyIconPath) };
+            indicationIcon.style.height = 30;
+            indicationIcon.style.width = 30;
+            indicationIcon.style.marginLeft = 2;
+            indicationIcon.style.marginTop = 1;
+            indicationIcon.style.marginRight = 5;
+            
+            indicationText = new Label(){text = EditorIndicationState.IndicationStatusText};
+            indicationText.style.paddingTop = 9;
+            indicationText.style.marginLeft = new StyleLength(StyleKeyword.Auto);
+            indicationText.style.marginRight = new StyleLength(StyleKeyword.Auto);
+            
+            root.Add(indicationIcon);
+            root.Add(indicationText);
+            root.style.width = 190;
+            root.style.height = 32;
+            initialized = true;
+            return root;
+        }
+
+        static bool _repaint;
+        static bool _instantRepaint;
+        static DateTime _lastRepaint;
+        private void Update() {
+            if (!initialized) {
+                return;
+            }
+            if (lastIndicationStatus != EditorIndicationState.CurrentIndicationStatus) {
+                indicationIcon.image = GetIndicationIcon();
+                indicationText.text = EditorIndicationState.IndicationStatusText;
+                lastIndicationStatus = EditorIndicationState.CurrentIndicationStatus;
+            }
+            try {
+                if (HotReloadEventPopup.I.open 
+                    && EditorWindow.mouseOverWindow
+                    && EditorWindow.mouseOverWindow?.GetType() == typeof(UnityEditor.PopupWindow)
+                ) {
+                    _repaint = true;
+                }
+            } catch (NullReferenceException) {
+                // Unity randomly throws nullrefs when EditorWindow.mouseOverWindow gets accessed
+            }
+            if (_repaint && DateTime.UtcNow - _lastRepaint > TimeSpan.FromMilliseconds(33)) {
+                _repaint = false;
+                _instantRepaint = true;
+            }
+            if (_instantRepaint) {
+                HotReloadEventPopup.I.Repaint();
+            }
+        }
+
+        ~HotReloadOverlay() {
+            EditorApplication.update -= Update;
+        }
+    }
+}
+#endif

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadOverlay.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 91650b4b0d054bdf9c1e922305e6a61a
+timeCreated: 1685130321

+ 478 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadPrefs.cs

@@ -0,0 +1,478 @@
+using System;
+using System.Globalization;
+using System.IO;
+using JetBrains.Annotations;
+using SingularityGroup.HotReload.Editor.Cli;
+using UnityEditor;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+    internal static class HotReloadPrefs {
+        private const string RemoteServerKey = "HotReloadWindow.RemoteServer";
+        private const string RemoteServerHostKey = "HotReloadWindow.RemoteServerHost";
+        private const string LicenseEmailKey = "HotReloadWindow.LicenseEmail";
+        private const string RenderAuthLoginKey = "HotReloadWindow.RenderAuthLogin";
+        private const string FirstLoginCachedKey = "HotReloadWindow.FirstLoginCachedKey";
+        [Obsolete]
+        private const string ShowOnStartupKey = "HotReloadWindow.ShowOnStartup";
+        private const string PasswordCachedKey = "HotReloadWindow.PasswordCached";
+        private const string ExposeServerToLocalNetworkKey = "HotReloadWindow.ExposeServerToLocalNetwork";
+        private const string ErrorHiddenCachedKey = "HotReloadWindow.ErrorHiddenCachedKey";
+        private const string RefreshManuallyTipCachedKey = "HotReloadWindow.RefreshManuallyTipCachedKey";
+        private const string ShowLoginCachedKey = "HotReloadWindow.ShowLoginCachedKey";
+        private const string ConfigurationKey = "HotReloadWindow.Configuration";
+        private const string AvdancedKey = "HotReloadWindow.Avdanced";
+        private const string ShowPromoCodesCachedKey = "HotReloadWindow.ShowPromoCodesCached";
+        private const string ShowOnDeviceKey = "HotReloadWindow.ShowOnDevice";
+        private const string ShowChangelogKey = "HotReloadWindow.ShowChangelog";
+        private const string UnsupportedChangesKey = "HotReloadWindow.ShowUnsupportedChanges";
+        private const string LoggedBurstHintKey = "HotReloadWindow.LoggedBurstHint";
+        private const string ShouldDoAutoRefreshFixupKey = "HotReloadWindow.ShouldDoAutoRefreshFixup";
+        private const string ActiveDaysKey = "HotReloadWindow.ActiveDays";
+        [Obsolete]
+        private const string RateAppShownKey = "HotReloadWindow.RateAppShown";
+        private const string PatchesCollapseKey = "HotReloadWindow.PatchesCollapse";
+        private const string PatchesGroupAllKey = "HotReloadWindow.PatchesGroupAll";
+        private const string LaunchOnEditorStartKey = "HotReloadWindow.LaunchOnEditorStart";
+        private const string AutoRecompileUnsupportedChangesKey = "HotReloadWindow.AutoRecompileUnsupportedChanges";
+        private const string AutoRecompilePartiallyUnsupportedChangesKey = "HotReloadWindow.AutoRecompilePartiallyUnsupportedChanges";
+        private const string DisplayNewMonobehaviourMethodsAsPartiallySupportedKey = "HotReloadWindow.DisplayNewMonobehaviourMethodsAsPartiallySupported";
+        private const string ShowNotificationsKey = "HotReloadWindow.ShowNotifications";
+        private const string ShowPatchingNotificationsKey = "HotReloadWindow.ShowPatchingNotifications";
+        private const string ShowCompilingUnsupportedNotificationsKey = "HotReloadWindow.ShowCompilingUnsupportedNotifications";
+        private const string AutoRecompileUnsupportedChangesImmediatelyKey = "HotReloadWindow.AutoRecompileUnsupportedChangesImmediately";
+        private const string AutoRecompileUnsupportedChangesOnExitPlayModeKey = "HotReloadWindow.AutoRecompileUnsupportedChangesOnExitPlayMode";
+        private const string AutoRecompileUnsupportedChangesInPlayModeKey = "HotReloadWindow.AutoRecompileUnsupportedChangesInPlayMode";
+        private const string AllowDisableUnityAutoRefreshKey = "HotReloadWindow.AllowDisableUnityAutoRefresh";
+        private const string DefaultAutoRefreshKey = "HotReloadWindow.DefaultAutoRefresh";
+        private const string DefaultAutoRefreshModeKey = "HotReloadWindow.DefaultAutoRefreshMode";
+        private const string DefaultScriptCompilationKeyKey = "HotReloadWindow.DefaultScriptCompilationKey";
+        private const string DefaultEditorTintKey = "HotReloadWindow.DefaultEditorTint";
+        private const string AppliedAutoRefreshKey = "HotReloadWindow.AppliedAutoRefresh";
+        private const string AppliedScriptCompilationKey = "HotReloadWindow.AppliedScriptCompilation";
+        private const string AppliedEditorTintKey = "HotReloadWindow.AppliedEditorTint";
+        private const string AllAssetChangesKey = "HotReloadWindow.AllAssetChanges";
+        private const string IncludeShaderChangesKey = "HotReloadWindow.IncludeShaderChanges";
+        private const string DisableConsoleWindowKey = "HotReloadWindow.DisableConsoleWindow";
+        private const string DisableDetailedErrorReportingKey = "HotReloadWindow.DisableDetailedErrorReporting";
+        private const string DebuggerCompatibilityEnabledKey = "HotReloadWindow.DebuggerCompatibilityEnabled";
+        private const string RedeemLicenseEmailKey = "HotReloadWindow.RedeemLicenseEmail";
+        private const string RedeemLicenseInvoiceKey = "HotReloadWindow.RedeemLicenseInvoice";
+        private const string RunTabEventsSuggestionsFoldoutKey = "HotReloadWindow.RunTabEventsSuggestionsFoldout";
+        private const string RunTabEventsTimelineFoldoutKey = "HotReloadWindow.RunTabEventsTimelineFoldout";
+        private const string RunTabUnsupportedChangesFilterKey = "HotReloadWindow.RunTabUnsupportedChangesFilter";
+        private const string RunTabCompileErrorFilterKey = "HotReloadWindow.RunTabCompileErrorFilter";
+        private const string RunTabPartiallyAppliedPatchesFilterKey = "HotReloadWindow.RunTabPartiallyAppliedPatchesFilter";
+        private const string RunTabUndetectedPatchesFilterKey = "HotReloadWindow.RunTabUndetectedPatchesFilter";
+        private const string RunTabAppliedPatchesFilterKey = "HotReloadWindow.RunTabAppliedPatchesFilter";
+        private const string RecompileDialogueShownKey = "HotReloadWindow.RecompileDialogueShown";
+        private const string ApplyFieldInitiailzerEditsToExistingClassInstancesKey = "HotReloadWindow.ApplyFieldInitiailzerEditsToExistingClassInstances";
+        private const string LoggedInlinedMethodsDialogueKey = "HotReloadWindow.LoggedInlinedMethodsDialogue";
+        private const string OpenedWindowAtLeastOnceKey = "HotReloadWindow.OpenedWindowAtLeastOnce";
+        private const string DeactivateHotReloadKey = "HotReloadWindow.DeactivateHotReload";
+
+        public const string DontShowPromptForDownloadKey = "ServerDownloader.DontShowPromptForDownload";
+
+        [Obsolete] public const string AllowHttpSettingCacheKey = "HotReloadWindow.AllowHttpSettingCacheKey";
+        [Obsolete] public const string AutoRefreshSettingCacheKey = "HotReloadWindow.AutoRefreshSettingCacheKey";
+        [Obsolete] public const string ScriptCompilationSettingCacheKey = "HotReloadWindow.ScriptCompilationSettingCacheKey";
+        [Obsolete] public const string ProjectGenerationSettingCacheKey = "HotReloadWindow.ProjectGenerationSettingCacheKey";
+
+
+        [Obsolete]
+        public static bool RemoteServer {
+            get { return EditorPrefs.GetBool(RemoteServerKey, false); }
+            set { EditorPrefs.SetBool(RemoteServerKey, value); }
+        }
+        
+        public static bool DontShowPromptForDownload {
+            get { return EditorPrefs.GetBool(DontShowPromptForDownloadKey, false); }
+            set { EditorPrefs.SetBool(DontShowPromptForDownloadKey, value); }
+        }
+
+        [Obsolete]
+        public static string RemoteServerHost {
+            get { return EditorPrefs.GetString(RemoteServerHostKey); }
+            set { EditorPrefs.SetString(RemoteServerHostKey, value); }
+        }
+
+        public static string LicenseEmail {
+            get { return EditorPrefs.GetString(LicenseEmailKey); }
+            set { EditorPrefs.SetString(LicenseEmailKey, value); }
+        }
+        
+        public static string LicensePassword {
+            get { return EditorPrefs.GetString(PasswordCachedKey); }
+            set { EditorPrefs.SetString(PasswordCachedKey, value); }
+        }
+        
+        [Obsolete]
+        public static bool RenderAuthLogin { // false = render free trial
+            get { return EditorPrefs.GetBool(RenderAuthLoginKey); }
+            set { EditorPrefs.SetBool(RenderAuthLoginKey, value); }
+        }
+        
+        [Obsolete]
+        public static bool FirstLogin {
+            get { return EditorPrefs.GetBool(FirstLoginCachedKey, true); }
+            set { EditorPrefs.SetBool(FirstLoginCachedKey, value); }
+        }
+
+        [Obsolete]
+        public static string ShowOnStartupLegacy { // WindowAutoOpen
+            get { return EditorPrefs.GetString(ShowOnStartupKey); }
+            set { EditorPrefs.SetString(ShowOnStartupKey, value); }
+        }
+        
+        public static string showOnStartupPath { get; }= Path.Combine(CliUtils.GetAppDataPath(), "showOnStartup.txt");
+        static ShowOnStartupEnum? showOnStartup;
+        public static ShowOnStartupEnum ShowOnStartup {
+            get {
+                if (showOnStartup != null) {
+                    return showOnStartup.Value;
+                }
+                if (!File.Exists(showOnStartupPath)) {
+                    showOnStartup = ShowOnStartupEnum.Always;
+                    return showOnStartup.Value;
+                }
+                var text = File.ReadAllText(showOnStartupPath);
+                ShowOnStartupEnum _showOnStartup;
+                if (Enum.TryParse(text, true, out _showOnStartup)) {
+                    showOnStartup = _showOnStartup;
+                    return showOnStartup.Value;
+                }
+                showOnStartup = ShowOnStartupEnum.Always;
+                return showOnStartup.Value;
+            }
+            set {
+                // ReSharper disable once AssignNullToNotNullAttribute
+                Directory.CreateDirectory(Path.GetDirectoryName(showOnStartupPath));
+                File.WriteAllText(showOnStartupPath, value.ToString());
+                showOnStartup = value;
+            }
+        }
+
+
+        public static bool ErrorHidden {
+            get { return EditorPrefs.GetBool(ErrorHiddenCachedKey); }
+            set { EditorPrefs.SetBool(ErrorHiddenCachedKey, value); }
+        }
+        
+        public static bool ShowLogin {
+            get { return EditorPrefs.GetBool(ShowLoginCachedKey, true); }
+            set { EditorPrefs.SetBool(ShowLoginCachedKey, value); }
+        }
+
+        public static bool ShowConfiguration {
+            get { return EditorPrefs.GetBool(ConfigurationKey, true); }
+            set { EditorPrefs.SetBool(ConfigurationKey, value); }
+        }
+        
+        public static bool ShowAdvanced {
+            get { return EditorPrefs.GetBool(AvdancedKey, false); }
+            set { EditorPrefs.SetBool(AvdancedKey, value); }
+        }
+
+        public static bool ShowPromoCodes {
+            get { return EditorPrefs.GetBool(ShowPromoCodesCachedKey, true); }
+            set { EditorPrefs.SetBool(ShowPromoCodesCachedKey, value); }
+        }
+        
+        public static bool ShowOnDevice {
+            get { return EditorPrefs.GetBool(ShowOnDeviceKey, true); }
+            set { EditorPrefs.SetBool(ShowOnDeviceKey, value); }
+        }
+        
+        public static bool ShowChangeLog {
+            get { return EditorPrefs.GetBool(ShowChangelogKey, true); }
+            set { EditorPrefs.SetBool(ShowChangelogKey, value); }
+        }
+        
+        public static bool ShowUnsupportedChanges {
+            get { return EditorPrefs.GetBool(UnsupportedChangesKey, true); }
+            set { EditorPrefs.SetBool(UnsupportedChangesKey, value); }
+        }
+        
+        [Obsolete]
+        public static bool RefreshManuallyTip {
+            get { return EditorPrefs.GetBool(RefreshManuallyTipCachedKey); }
+            set { EditorPrefs.SetBool(RefreshManuallyTipCachedKey, value); }
+        }
+        
+        public static bool LoggedBurstHint {
+            get { return EditorPrefs.GetBool(LoggedBurstHintKey); }
+            set { EditorPrefs.SetBool(LoggedBurstHintKey, value); }
+        }
+        
+        [Obsolete]
+        public static bool ShouldDoAutoRefreshFixup {
+            get { return EditorPrefs.GetBool(ShouldDoAutoRefreshFixupKey, true); }
+            set { EditorPrefs.SetBool(ShouldDoAutoRefreshFixupKey, value); }
+        }
+        
+        public static string ActiveDays {
+            get { return EditorPrefs.GetString(ActiveDaysKey, string.Empty); }
+            set { EditorPrefs.SetString(ActiveDaysKey, value); }
+        }
+        
+        [Obsolete]
+        public static bool RateAppShownLegacy {
+            get { return EditorPrefs.GetBool(RateAppShownKey, false); }
+            set { EditorPrefs.SetBool(RateAppShownKey, value); }
+        }
+        
+        static string rateAppPath = Path.Combine(CliUtils.GetAppDataPath(), "ratedApp.txt");
+        static bool? rateAppShown;
+        public static bool RateAppShown {
+            get {
+                if (rateAppShown != null) {
+                    return rateAppShown.Value;
+                }
+                rateAppShown = File.Exists(rateAppPath);
+                return rateAppShown.Value;
+            }
+            set {
+                // ReSharper disable once AssignNullToNotNullAttribute
+                Directory.CreateDirectory(Path.GetDirectoryName(rateAppPath));
+                if (value && !File.Exists(rateAppPath)) {
+                    using (File.Create(rateAppPath)) { }
+                } else if (!value && File.Exists(rateAppPath)) {
+                    File.Delete(rateAppPath);
+                }
+                rateAppShown = value;
+            }
+        }
+
+        [Obsolete]
+        public static bool PatchesGroupAll {
+            get { return EditorPrefs.GetBool(PatchesGroupAllKey, false); }
+            set { EditorPrefs.SetBool(PatchesGroupAllKey, value); }
+        }
+
+        [Obsolete]
+        public static bool PatchesCollapse {
+            get { return EditorPrefs.GetBool(PatchesCollapseKey, true); }
+            set { EditorPrefs.SetBool(PatchesCollapseKey, value); }
+        }
+
+        [Obsolete]
+        public static ShowOnStartupEnum GetShowOnStartupEnum() {
+            ShowOnStartupEnum showOnStartupEnum;
+            if (Enum.TryParse(HotReloadPrefs.ShowOnStartupLegacy, true, out showOnStartupEnum)) {
+                return showOnStartupEnum;
+            }
+            return ShowOnStartupEnum.Always;
+        }
+        
+        public static bool ExposeServerToLocalNetwork {
+            get { return EditorPrefs.GetBool(ExposeServerToLocalNetworkKey, false); }
+            set { EditorPrefs.SetBool(ExposeServerToLocalNetworkKey, value); }
+        }
+        
+        public static bool LaunchOnEditorStart {
+            get { return EditorPrefs.GetBool(LaunchOnEditorStartKey, false); }
+            set { EditorPrefs.SetBool(LaunchOnEditorStartKey, value); }
+        }
+
+        public static bool AutoRecompileUnsupportedChanges {
+            get { return EditorPrefs.GetBool(AutoRecompileUnsupportedChangesKey, false); }
+            set { EditorPrefs.SetBool(AutoRecompileUnsupportedChangesKey, value); }
+        }
+        
+        public static bool AutoRecompilePartiallyUnsupportedChanges {
+            get { return EditorPrefs.GetBool(AutoRecompilePartiallyUnsupportedChangesKey, false); }
+            set { EditorPrefs.SetBool(AutoRecompilePartiallyUnsupportedChangesKey, value); }
+        }
+        
+        public static bool DisplayNewMonobehaviourMethodsAsPartiallySupported {
+            get { return EditorPrefs.GetBool(DisplayNewMonobehaviourMethodsAsPartiallySupportedKey, false); }
+            set { EditorPrefs.SetBool(DisplayNewMonobehaviourMethodsAsPartiallySupportedKey, value); }
+        }
+
+        public static bool ShowNotifications {
+            get { return EditorPrefs.GetBool(ShowNotificationsKey, true); }
+            set { EditorPrefs.SetBool(ShowNotificationsKey, value); }
+        }
+
+        public static bool ShowPatchingNotifications {
+            get { return EditorPrefs.GetBool(ShowPatchingNotificationsKey, true); }
+            set { EditorPrefs.SetBool(ShowPatchingNotificationsKey, value); }
+        }
+
+        public static bool ShowCompilingUnsupportedNotifications {
+            get { return EditorPrefs.GetBool(ShowCompilingUnsupportedNotificationsKey, true); }
+            set { EditorPrefs.SetBool(ShowCompilingUnsupportedNotificationsKey, value); }
+        }
+
+        public static bool AutoRecompileUnsupportedChangesImmediately {
+            get { return EditorPrefs.GetBool(AutoRecompileUnsupportedChangesImmediatelyKey, false); }
+            set { EditorPrefs.SetBool(AutoRecompileUnsupportedChangesImmediatelyKey, value); }
+        }
+        
+        public static bool AutoRecompileUnsupportedChangesOnExitPlayMode {
+            get { return EditorPrefs.GetBool(AutoRecompileUnsupportedChangesOnExitPlayModeKey, false); }
+            set { EditorPrefs.SetBool(AutoRecompileUnsupportedChangesOnExitPlayModeKey, value); }
+        }
+        
+        public static bool AutoRecompileUnsupportedChangesInPlayMode {
+            get { return EditorPrefs.GetBool(AutoRecompileUnsupportedChangesInPlayModeKey, false); }
+            set { EditorPrefs.SetBool(AutoRecompileUnsupportedChangesInPlayModeKey, value); }
+        }
+
+        public static bool AllowDisableUnityAutoRefresh {
+            get { return EditorPrefs.GetBool(AllowDisableUnityAutoRefreshKey, false); }
+            set { EditorPrefs.SetBool(AllowDisableUnityAutoRefreshKey, value); }
+        }
+        
+        public static int DefaultAutoRefresh {
+            get { return EditorPrefs.GetInt(DefaultAutoRefreshKey, -1); }
+            set { EditorPrefs.SetInt(DefaultAutoRefreshKey, value); }
+        }
+        
+        [UsedImplicitly]
+        public static int DefaultAutoRefreshMode {
+            get { return EditorPrefs.GetInt(DefaultAutoRefreshModeKey, -1); }
+            set { EditorPrefs.SetInt(DefaultAutoRefreshModeKey, value); }
+        }
+        
+        public static int DefaultScriptCompilation {
+            get { return EditorPrefs.GetInt(DefaultScriptCompilationKeyKey, -1); }
+            set { EditorPrefs.SetInt(DefaultScriptCompilationKeyKey, value); }
+        }
+        
+        public static Color? DefaultEditorTint {
+            get { return ColorFromString(EditorPrefs.GetString(DefaultEditorTintKey, string.Empty)); }
+            set { EditorPrefs.SetString(DefaultEditorTintKey, ColorToString(value)); }
+        }
+        
+        public static bool AppliedAutoRefresh {
+            get { return EditorPrefs.GetBool(AppliedAutoRefreshKey); }
+            set { EditorPrefs.SetBool(AppliedAutoRefreshKey, value); }
+        }
+        
+        public static bool AppliedScriptCompilation {
+            get { return EditorPrefs.GetBool(AppliedScriptCompilationKey); }
+            set { EditorPrefs.SetBool(AppliedScriptCompilationKey, value); }
+        }
+        
+        public static Color? AppliedEditorTint {
+            get { return ColorFromString(EditorPrefs.GetString(AppliedEditorTintKey, string.Empty)); }
+            set { EditorPrefs.SetString(AppliedEditorTintKey, ColorToString(value)); }
+        }
+        
+        public static bool AllAssetChanges {
+            get { return EditorPrefs.GetBool(AllAssetChangesKey, false); }
+            set { EditorPrefs.SetBool(AllAssetChangesKey, value); }
+        }
+        
+        public static bool IncludeShaderChanges {
+            get { return EditorPrefs.GetBool(IncludeShaderChangesKey, false); }
+            set { EditorPrefs.SetBool(IncludeShaderChangesKey, value); }
+        }
+        
+        public static bool DisableConsoleWindow {
+            get { return EditorPrefs.GetBool(DisableConsoleWindowKey, false); }
+            set { EditorPrefs.SetBool(DisableConsoleWindowKey, value); }
+        }
+        
+        public static string RedeemLicenseEmail {
+            get { return EditorPrefs.GetString(RedeemLicenseEmailKey); }
+            set { EditorPrefs.SetString(RedeemLicenseEmailKey, value); }
+        }
+        
+        public static string RedeemLicenseInvoice {
+            get { return EditorPrefs.GetString(RedeemLicenseInvoiceKey); }
+            set { EditorPrefs.SetString(RedeemLicenseInvoiceKey, value); }
+        }
+        
+        public static bool RunTabEventsTimelineFoldout {
+            get { return EditorPrefs.GetBool(RunTabEventsTimelineFoldoutKey, true); }
+            set { EditorPrefs.SetBool(RunTabEventsTimelineFoldoutKey, value); }
+        }
+        
+        public static bool RunTabEventsSuggestionsFoldout {
+            get { return EditorPrefs.GetBool(RunTabEventsSuggestionsFoldoutKey, true); }
+            set { EditorPrefs.SetBool(RunTabEventsSuggestionsFoldoutKey, value); }
+        }
+        
+        public static bool RunTabUnsupportedChangesFilter {
+            get { return EditorPrefs.GetBool(RunTabUnsupportedChangesFilterKey, true); }
+            set { EditorPrefs.SetBool(RunTabUnsupportedChangesFilterKey, value); }
+        }
+        
+        public static bool RunTabCompileErrorFilter {
+            get { return EditorPrefs.GetBool(RunTabCompileErrorFilterKey, true); }
+            set { EditorPrefs.SetBool(RunTabCompileErrorFilterKey, value); }
+        }
+        
+        public static bool RunTabPartiallyAppliedPatchesFilter {
+            get { return EditorPrefs.GetBool(RunTabPartiallyAppliedPatchesFilterKey, true); }
+            set { EditorPrefs.SetBool(RunTabPartiallyAppliedPatchesFilterKey, value); }
+        }
+        
+        public static bool RunTabUndetectedPatchesFilter {
+            get { return EditorPrefs.GetBool(RunTabUndetectedPatchesFilterKey, true); }
+            set { EditorPrefs.SetBool(RunTabUndetectedPatchesFilterKey, value); }
+        }
+        
+        public static bool RunTabAppliedPatchesFilter {
+            get { return EditorPrefs.GetBool(RunTabAppliedPatchesFilterKey, true); }
+            set { EditorPrefs.SetBool(RunTabAppliedPatchesFilterKey, value); }
+        }
+        
+        public static bool RecompileDialogueShown {
+            get { return EditorPrefs.GetBool(RecompileDialogueShownKey); }
+            set { EditorPrefs.SetBool(RecompileDialogueShownKey, value); }
+        }
+        
+        public static bool OpenedWindowAtLeastOnce {
+            get { return EditorPrefs.GetBool(OpenedWindowAtLeastOnceKey); }
+            set { EditorPrefs.SetBool(OpenedWindowAtLeastOnceKey, value); }
+        }
+        
+        private const string rgbaDelimiter = ";";
+        public static string ColorToString(Color? _color) {
+            if (_color == null) {
+                return null;
+            }
+            var color = _color.Value;
+            var cultInfo = CultureInfo.InvariantCulture;
+            string[] rgbaList = { color.r.ToString(cultInfo), color.g.ToString(cultInfo), color.b.ToString(cultInfo), color.a.ToString(cultInfo)};
+            return String.Join(rgbaDelimiter, rgbaList);
+        }
+
+        public static Color? ColorFromString(string ser) {
+            if (string.IsNullOrEmpty(ser)) {
+                return null;
+            }
+            string[] rgbaParts = ser.Split(rgbaDelimiter.ToCharArray());
+            return new Color(float.Parse(rgbaParts[0]), float.Parse(rgbaParts[1]),float.Parse(rgbaParts[2]),float.Parse(rgbaParts[3]));
+        }
+        
+        [Obsolete("was not implemented")]
+        public static bool ApplyFieldInitiailzerEditsToExistingClassInstances {
+            get { return EditorPrefs.GetBool(ApplyFieldInitiailzerEditsToExistingClassInstancesKey); }
+            set { EditorPrefs.SetBool(ApplyFieldInitiailzerEditsToExistingClassInstancesKey, value); }
+        }
+        
+        public static bool LoggedInlinedMethodsDialogue {
+            get { return EditorPrefs.GetBool(LoggedInlinedMethodsDialogueKey); }
+            set { EditorPrefs.SetBool(LoggedInlinedMethodsDialogueKey, value); }
+        }
+        
+        public static bool DeactivateHotReload {
+            get { return EditorPrefs.GetBool(DeactivateHotReloadKey); }
+            set { EditorPrefs.SetBool(DeactivateHotReloadKey, value); }
+        }
+        
+        public static bool DisableDetailedErrorReporting {
+            get { return EditorPrefs.GetBool(DisableDetailedErrorReportingKey, false); }
+            set { EditorPrefs.SetBool(DisableDetailedErrorReportingKey, value); }
+        }
+        
+        public static bool AutoDisableHotReloadWithDebugger {
+            get { return EditorPrefs.GetBool(DebuggerCompatibilityEnabledKey, true); }
+            set { EditorPrefs.SetBool(DebuggerCompatibilityEnabledKey, value); }
+        }
+    }
+}

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadPrefs.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 96451431b50143944b85d4fbdde5f104
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 70 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadSettingsEditor.cs

@@ -0,0 +1,70 @@
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+    static class HotReloadSettingsEditor {
+        /// Ensure settings asset file is created and saved
+        public static void EnsureSettingsCreated(HotReloadSettingsObject asset) {
+            if (!SettingsExists()) {
+                CreateNewSettingsFile(asset, HotReloadSettingsObject.editorAssetPath);
+            }
+        }
+
+        /// Load existing settings asset or return the default settings
+        public static HotReloadSettingsObject LoadSettingsOrDefault() {
+            if (SettingsExists()) {
+                return AssetDatabase.LoadAssetAtPath<HotReloadSettingsObject>(HotReloadSettingsObject.editorAssetPath);
+            } else {
+                // create an instance with default values
+                return ScriptableObject.CreateInstance<HotReloadSettingsObject>();
+            }
+        }
+
+        /// <summary>
+        /// Create settings asset file
+        /// </summary>
+        /// <remarks>Assume that settings asset doesn't exist yet</remarks>
+        /// <returns>The settings asset</returns>
+        static void CreateNewSettingsFile(HotReloadSettingsObject asset, string editorAssetPath) {
+            // create new settings asset
+            // ReSharper disable once AssignNullToNotNullAttribute
+            Directory.CreateDirectory(Path.GetDirectoryName(editorAssetPath));
+            if (asset == null) {
+                asset = ScriptableObject.CreateInstance<HotReloadSettingsObject>();
+            }
+            AssetDatabase.CreateAsset(asset, editorAssetPath);
+            // Saving the asset isn't needed right after you created it. Unity will save it at the appropriate time.
+            // Troy: I tested in Unity 2018 LTS, first Android build creates the asset file and asset is included in the build.
+        }
+
+        #region include/exclude in build
+
+        private static bool SettingsExists() {
+            return AssetExists(HotReloadSettingsObject.editorAssetPath);
+        }
+
+        private static bool AssetExists(string assetPath) {
+            return AssetDatabase.GetMainAssetTypeAtPath(assetPath) != null;
+        }
+
+        public static void AddOrRemoveFromBuild(bool includeSettingsInBuild) {
+            AssetDatabase.StartAssetEditing();
+            var so = LoadSettingsOrDefault();
+            try {
+                if (includeSettingsInBuild) {
+                    // Note: don't need to force create settings because we know the defaults in player.
+                    so.EnsurePrefabSetCorrectly();
+                    EnsureSettingsCreated(so);
+                } else {
+                    // this block shouldn't create the asset file, but it's also fine if it does
+                    so.EnsurePrefabNotInBuild();
+                }
+            } finally {
+                AssetDatabase.StopAssetEditing();
+            }
+        }
+
+        #endregion
+    }
+}

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadSettingsEditor.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a0f4231ca4f63e54da0ecf87ab62c381
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 80 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadState.cs

@@ -0,0 +1,80 @@
+using UnityEditor;
+
+namespace SingularityGroup.HotReload.Editor {
+    internal static class HotReloadState {
+        private const string ServerPortKey = "HotReloadWindow.ServerPort";
+        private const string LastPatchIdKey = "HotReloadWindow.LastPatchId";
+        private const string ShowingRedDotKey = "HotReloadWindow.ShowingRedDot";
+        private const string ShowedEditorsWithoutHRKey = "HotReloadWindow.ShowedEditorWithoutHR";
+        private const string ShowedFieldInitializerWithSideEffectsKey = "HotReloadWindow.ShowedFieldInitializerWithSideEffects";
+        private const string ShowedAddMonobehaviourMethodsKey = "HotReloadWindow.ShowedAddMonobehaviourMethods";
+        private const string ShowedFieldInitializerExistingInstancesEditedKey = "HotReloadWindow.ShowedFieldInitializerExistingInstancesEdited";
+        private const string ShowedFieldInitializerExistingInstancesUneditedKey = "HotReloadWindow.ShowedFieldInitializerExistingInstancesUnedited";
+        private const string RecompiledUnsupportedChangesOnExitPlaymodeKey = "HotReloadWindow.RecompiledUnsupportedChangesOnExitPlaymode";
+        private const string RecompiledUnsupportedChangesInPlaymodeKey = "HotReloadWindow.RecompiledUnsupportedChangesInPlaymode";
+        private const string EditorCodePatcherInitKey = "HotReloadWindow.EditorCodePatcherInit";
+        private const string ShowedDebuggerCompatibilityKey = "HotReloadWindow.ShowedDebuggerCompatibility";
+        
+
+        public static int ServerPort {
+            get { return SessionState.GetInt(ServerPortKey, RequestHelper.defaultPort); }
+            set { SessionState.SetInt(ServerPortKey, value); }
+        }
+        
+        public static string LastPatchId {
+            get { return SessionState.GetString(LastPatchIdKey, string.Empty); }
+            set { SessionState.SetString(LastPatchIdKey, value); }
+        }
+        
+        public static bool ShowingRedDot {
+            get { return SessionState.GetBool(ShowingRedDotKey, false); }
+            set { SessionState.SetBool(ShowingRedDotKey, value); }
+        }
+        
+        public static bool ShowedEditorsWithoutHR {
+            get { return SessionState.GetBool(ShowedEditorsWithoutHRKey, false); }
+            set { SessionState.SetBool(ShowedEditorsWithoutHRKey, value); }
+        }
+        
+        public static bool ShowedFieldInitializerWithSideEffects {
+            get { return SessionState.GetBool(ShowedFieldInitializerWithSideEffectsKey, false); }
+            set { SessionState.SetBool(ShowedFieldInitializerWithSideEffectsKey, value); }
+        }
+        
+        public static bool ShowedAddMonobehaviourMethods {
+            get { return SessionState.GetBool(ShowedAddMonobehaviourMethodsKey, false); }
+            set { SessionState.SetBool(ShowedAddMonobehaviourMethodsKey, value); }
+        }
+        
+        public static bool ShowedFieldInitializerExistingInstancesEdited {
+            get { return SessionState.GetBool(ShowedFieldInitializerExistingInstancesEditedKey, false); }
+            set { SessionState.SetBool(ShowedFieldInitializerExistingInstancesEditedKey, value); }
+        }
+        
+        public static bool ShowedFieldInitializerExistingInstancesUnedited {
+            get { return SessionState.GetBool(ShowedFieldInitializerExistingInstancesUneditedKey, false); }
+            set { SessionState.SetBool(ShowedFieldInitializerExistingInstancesUneditedKey, value); }
+        }
+        
+        public static bool RecompiledUnsupportedChangesOnExitPlaymode {
+            get { return SessionState.GetBool(RecompiledUnsupportedChangesOnExitPlaymodeKey, false); }
+            set { SessionState.SetBool(RecompiledUnsupportedChangesOnExitPlaymodeKey, value); }
+        }
+        
+        public static bool RecompiledUnsupportedChangesInPlaymode {
+            get { return SessionState.GetBool(RecompiledUnsupportedChangesInPlaymodeKey, false); }
+            set { SessionState.SetBool(RecompiledUnsupportedChangesInPlaymodeKey, value); }
+        }
+        
+        public static bool EditorCodePatcherInit {
+            get { return SessionState.GetBool(EditorCodePatcherInitKey, false); }
+            set { SessionState.SetBool(EditorCodePatcherInitKey, value); }
+        }
+        
+        public static bool ShowedDebuggerCompatibility {
+            get { return SessionState.GetBool(ShowedDebuggerCompatibilityKey, false); }
+            set { SessionState.SetBool(ShowedDebuggerCompatibilityKey, value); }
+        }
+    }
+
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/HotReloadState.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 803347281bcf46b6b37d48231b8882be
+timeCreated: 1694458889

BIN
Packages/com.singularitygroup.hotreload/Editor/Icon_Player.png


+ 147 - 0
Packages/com.singularitygroup.hotreload/Editor/Icon_Player.png.meta

@@ -0,0 +1,147 @@
+fileFormatVersion: 2
+guid: 90cf8e542151548c6aa3cba26467e144
+TextureImporter:
+  internalIDToNameTable: []
+  externalObjects: {}
+  serializedVersion: 12
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    sRGBTexture: 1
+    linearTexture: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapsPreserveCoverage: 0
+    alphaTestReferenceValue: 0.5
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  streamingMipmaps: 0
+  streamingMipmapsPriority: 0
+  vTOnly: 0
+  ignoreMasterTextureLimit: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: 1
+  maxTextureSize: 2048
+  textureSettings:
+    serializedVersion: 2
+    filterMode: 1
+    aniso: 1
+    mipBias: 0
+    wrapU: 1
+    wrapV: 1
+    wrapW: 1
+  nPOTScale: 0
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 1
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spritePixelsToUnits: 100
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spriteGenerateFallbackPhysicsShape: 1
+  alphaUsage: 1
+  alphaIsTransparency: 1
+  spriteTessellationDetail: -1
+  textureType: 8
+  textureShape: 1
+  singleChannelComponent: 0
+  flipbookRows: 1
+  flipbookColumns: 1
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  ignorePngGamma: 0
+  applyGammaDecoding: 0
+  cookieLightType: 0
+  platformSettings:
+  - serializedVersion: 3
+    buildTarget: DefaultTexturePlatform
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: Standalone
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: Server
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: Android
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: iPhone
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+    physicsShape: []
+    bones: []
+    spriteID: 5e97eb03825dee720800000000000000
+    internalID: 0
+    vertices: []
+    indices: 
+    edges: []
+    weights: []
+    secondaryTextures: []
+    nameFileIdTable: {}
+  spritePackingTag: 
+  pSDRemoveMatte: 0
+  pSDShowRemoveMatteOption: 0
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 117 - 0
Packages/com.singularitygroup.hotreload/Editor/InspectorFreezeFix.cs

@@ -0,0 +1,117 @@
+using System.Reflection;
+using SingularityGroup.HotReload.Editor;
+using UnityEditor;
+using UnityEngine;
+
+[InitializeOnLoad]
+public class InspectorFreezeFix
+{
+    // Inspector window getting stuck is fixed by calling UnityEditor.InspectorWindow.RefreshInspectors()
+    // Below code subscribes to selection changed callback and calls the method if the inspector is actually stuck
+
+    static InspectorFreezeFix()
+    {
+        Selection.selectionChanged += OnSelectionChanged;
+    }
+    
+    private static int _lastInitialEditorId;
+
+    private static void OnSelectionChanged() {
+        if (!EditorCodePatcher.config.enableInspectorFreezeFix) {
+            return;
+        }
+        try {
+            // Most of stuff is internal so we use reflection here
+            var inspectorType = typeof(Editor).Assembly.GetType("UnityEditor.InspectorWindow");
+
+            foreach (var inspector in Resources.FindObjectsOfTypeAll(inspectorType)) {
+                
+                object isLockedValue = inspectorType.GetProperty("isLocked")?.GetValue(inspector);
+                if (isLockedValue == null) {
+                    continue;
+                }
+                
+                // If inspector window is locked deliberately by user (via the lock icon on top-right), we don't need to refresh
+                var isLocked = (bool)isLockedValue;
+                if (isLocked) {
+                    continue;
+                }
+                
+                // Inspector getting stuck is due to ActiveEditorTracker of that window getting stuck internally.
+                // The tracker starts returning same values from m_Tracker.activeEditors property.
+                // (Root of cause of this is out of my reach as the tracker code is mainly native code)
+
+                // We detect that by checking first element of activeEditors array
+                // We do the check because we don't want to RefreshInspectors every selection change, RefreshInspectors is expensive
+                var tracker = inspectorType.GetField("m_Tracker", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(inspector);
+                if (tracker == null) {
+                    continue;
+                }
+                var activeEditors = tracker.GetType().GetProperty("activeEditors");
+                if (activeEditors == null) {
+                    continue;
+                }
+                var editors = (Editor[])activeEditors.GetValue(tracker);
+                if (editors.Length == 0) {
+                    continue;
+                }
+                
+                var first = editors[0].GetInstanceID();
+                if (_lastInitialEditorId == first) {
+                    // This forces the tracker to be rebuilt
+                    var m = inspectorType.GetMethod("RefreshInspectors", BindingFlags.Static | BindingFlags.NonPublic);
+                    if (m == null) {
+                        // support for older versions
+                        RefreshInspectors(inspectorType);
+                    } else {
+                        m.Invoke(null, null);
+                    }
+                }
+                _lastInitialEditorId = first;
+                // Calling RefreshInspectors once refreshes all the editors
+                break;
+            }
+        } catch {
+            // ignore, we don't want to make user experience worse by displaying a warning in this case
+        }
+    }
+
+    static void RefreshInspectors(System.Type inspectorType) {
+        var allInspectorsField = inspectorType.GetField("m_AllInspectors", BindingFlags.NonPublic | BindingFlags.Static);
+        
+        if (allInspectorsField == null) {
+            return;
+        }
+        var allInspectors = allInspectorsField.GetValue(null) as System.Collections.IEnumerable;
+        if (allInspectors == null) {
+            return;
+        }
+        
+        foreach (var inspector in allInspectors) {
+            var trackerField = FindFieldInHierarchy(inspector.GetType(), "tracker");
+
+            if (trackerField == null) {
+                continue;
+            }
+            var tracker = trackerField.GetValue(inspector);
+            var forceRebuildMethod = tracker.GetType().GetMethod("ForceRebuild", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+            if (forceRebuildMethod == null) {
+                
+                continue;
+            }
+            forceRebuildMethod.Invoke(tracker, null);
+        }
+    }
+
+    static PropertyInfo FindFieldInHierarchy(System.Type type, string fieldName) {
+        PropertyInfo field = null;
+
+        while (type != null && field == null) {
+            field = type.GetProperty(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+            type = type.BaseType;
+        }
+
+        return field;
+    }
+}
+

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/InspectorFreezeFix.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 235343744f6348acb629d549ccafff0b
+timeCreated: 1708187279

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Installation.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 12e88a0f97924d18859867b0cc957d03
+timeCreated: 1676802469

+ 98 - 0
Packages/com.singularitygroup.hotreload/Editor/Installation/DownloadUtility.cs

@@ -0,0 +1,98 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using SingularityGroup.HotReload.Editor.Cli;
+
+namespace SingularityGroup.HotReload.Editor {
+    static class DownloadUtility {
+        const string baseUrl = "https://cdn.hotreload.net";
+        
+        public static async Task<DownloadResult> DownloadFile(string url, string targetFilePath, IProgress<float> progress, CancellationToken cancellationToken) {
+            var tmpDir = Path.GetDirectoryName(targetFilePath);
+            Directory.CreateDirectory(tmpDir);
+            using(var client = HttpClientUtils.CreateHttpClient()) {
+                client.Timeout = TimeSpan.FromMinutes(10);
+                return await client.DownloadAsync(url, targetFilePath, progress, cancellationToken).ConfigureAwait(false);
+            }
+        }
+        
+        public static string GetPackagePrefix(string version) {
+            if (PackageConst.IsAssetStoreBuild) {
+                return $"releases/asset-store/{version.Replace('.', '-')}";
+            }
+            return $"releases/{version.Replace('.', '-')}";
+        }
+        
+        public static string GetDownloadUrl(string key) {
+            return $"{baseUrl}/{key}";
+        }
+        
+        public static async Task<DownloadResult> DownloadAsync(this HttpClient client, string requestUri, string destinationFilePath, IProgress<float> progress, CancellationToken cancellationToken = default(CancellationToken)) {
+            // Get the http headers first to examine the content length
+            using (var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false)) {
+                if (response.StatusCode != HttpStatusCode.OK) {
+                    throw new DownloadException($"Download failed with status code {response.StatusCode} and reason {response.ReasonPhrase}");
+                }
+                var contentLength = response.Content.Headers.ContentLength;
+                if (!contentLength.HasValue) {
+                    throw new DownloadException("Download failed: Content length unknown");
+                }
+    
+                using (var fs = new FileStream(destinationFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
+                using (var download = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) {
+    
+                    // Ignore progress reporting when no progress reporter was 
+                    if (progress == null) {
+                        await download.CopyToAsync(fs).ConfigureAwait(false);
+                    } else {
+                        // Convert absolute progress (bytes downloaded) into relative progress (0% - 99.9%)
+                        var relativeProgress = new Progress<long>(totalBytes => progress.Report(Math.Min(99.9f, (float)totalBytes / contentLength.Value)));
+                        // Use extension method to report progress while downloading
+                        await download.CopyToAsync(fs, 81920, relativeProgress, cancellationToken).ConfigureAwait(false);
+                    }
+                    await fs.FlushAsync().ConfigureAwait(false);
+                    if (fs.Length != contentLength.Value) {
+                        throw new DownloadException("Download failed: download file is corrupted");
+                    }
+                    return new DownloadResult(HttpStatusCode.OK, null);
+                }
+            }
+        }
+        
+        static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress, CancellationToken cancellationToken) {
+            if (source == null)
+                throw new ArgumentNullException(nameof(source));
+            if (!source.CanRead)
+                throw new ArgumentException("Has to be readable", nameof(source));
+            if (destination == null)
+                throw new ArgumentNullException(nameof(destination));
+            if (!destination.CanWrite)
+                throw new ArgumentException("Has to be writable", nameof(destination));
+            if (bufferSize < 0)
+                throw new ArgumentOutOfRangeException(nameof(bufferSize));
+
+            var buffer = new byte[bufferSize];
+            long totalBytesRead = 0;
+            int bytesRead;
+            while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) {
+                await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
+                totalBytesRead += bytesRead;
+                progress?.Report(totalBytesRead);
+            }
+        }
+
+        [Serializable]
+        public class DownloadException : ApplicationException {
+            public DownloadException(string message)
+                : base(message) {
+            }
+
+            public DownloadException(string message, Exception innerException)
+                : base(message, innerException) {
+            }
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Installation/DownloadUtility.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 2a7a39befa1f455cb21fcad46513b6e5
+timeCreated: 1676973096

+ 18 - 0
Packages/com.singularitygroup.hotreload/Editor/Installation/ExponentialBackoff.cs

@@ -0,0 +1,18 @@
+using System;
+
+namespace SingularityGroup.HotReload.Editor {
+    static class ExponentialBackoff {
+        
+        public static TimeSpan GetTimeout(int attempt, int minBackoff = 250, int maxBackoff = 60000, int deltaBackoff = 400) {
+            attempt = Math.Min(25, attempt); // safety to avoid overflow below
+
+            var delta = (uint)(
+                (Math.Pow(2.0, attempt) - 1.0)
+                * deltaBackoff 
+            );
+
+            var interval = Math.Min(checked(minBackoff + delta), maxBackoff);
+            return TimeSpan.FromMilliseconds(interval);
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Installation/ExponentialBackoff.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 5329de48151140eb871721ae80f925cd
+timeCreated: 1676908147

+ 66 - 0
Packages/com.singularitygroup.hotreload/Editor/Installation/InstallUtility.cs

@@ -0,0 +1,66 @@
+using System;
+using System.IO;
+using SingularityGroup.HotReload.DTO;
+using SingularityGroup.HotReload.Editor.Cli;
+using SingularityGroup.HotReload.EditorDependencies;
+using UnityEditor;
+using UnityEngine;
+#if UNITY_2019_4_OR_NEWER
+using System.Reflection;
+using Unity.CodeEditor;
+#endif
+
+namespace SingularityGroup.HotReload.Editor {
+    static class InstallUtility {
+        const string installFlagPath = PackageConst.LibraryCachePath + "/installFlag.txt";
+
+        public static void DebugClearInstallState() {
+            File.Delete(installFlagPath);
+        }
+
+        // HandleEditorStart is only called on editor start, not on domain reload
+        public static void HandleEditorStart(string updatedFromVersion) {
+            var showOnStartup = HotReloadPrefs.ShowOnStartup;
+            if (showOnStartup == ShowOnStartupEnum.Always || (showOnStartup == ShowOnStartupEnum.OnNewVersion && !String.IsNullOrEmpty(updatedFromVersion))) {
+                // Don't open Hot Reload window inside Virtual Player folder
+                // This is a heuristic since user might have the main player inside VP user-created folder, but that will be rare
+                if (new DirectoryInfo(Path.GetFullPath("..")).Name != "VP" && !HotReloadPrefs.DeactivateHotReload) {
+                    HotReloadWindow.Open();
+                }
+            }
+            if (HotReloadPrefs.LaunchOnEditorStart && !HotReloadPrefs.DeactivateHotReload) {
+                EditorCodePatcher.DownloadAndRun().Forget();
+            }
+            
+            RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Editor, StatEventType.Start)).Forget();
+        }
+
+        public static void CheckForNewInstall() {
+            if(File.Exists(installFlagPath)) {
+                return;
+            }
+            Directory.CreateDirectory(Path.GetDirectoryName(installFlagPath));
+            using(File.Create(installFlagPath)) { }
+            //Avoid opening the window on domain reload
+            EditorApplication.delayCall += HandleNewInstall;
+        }
+        
+        static void HandleNewInstall() {
+            if (EditorCodePatcher.licenseType == UnityLicenseType.UnityPro) {
+                RedeemLicenseHelper.I.StartRegistration();
+            }
+            // Don't open Hot Reload window inside Virtual Player folder
+            // This is a heuristic since user might have the main player inside VP user-created folder, but that will be rare
+            if (new DirectoryInfo(Path.GetFullPath("..")).Name != "VP") {
+                HotReloadWindow.Open();
+            }
+            HotReloadPrefs.AllowDisableUnityAutoRefresh = true;
+            HotReloadPrefs.AllAssetChanges = true;
+            HotReloadPrefs.AutoRecompileUnsupportedChanges = true;
+            HotReloadPrefs.AutoRecompileUnsupportedChangesOnExitPlayMode = true;
+            if (HotReloadCli.CanOpenInBackground) {
+                HotReloadPrefs.DisableConsoleWindow = true;
+            }
+        }
+    }
+}

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/Installation/InstallUtility.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ee93b2c98bc7d8f4bb38bbbd5961d354
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 190 - 0
Packages/com.singularitygroup.hotreload/Editor/Installation/ServerDownloader.cs

@@ -0,0 +1,190 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using SingularityGroup.HotReload.DTO;
+using SingularityGroup.HotReload.Editor.Cli;
+using SingularityGroup.HotReload.Newtonsoft.Json;
+using UnityEditor;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+    internal class ServerDownloader : IProgress<float> {
+        public float Progress {get; private set;}
+        public bool Started {get; private set;}
+
+        class Config {
+            public Dictionary<string, string> customServerExecutables;
+        }
+        
+        public string GetExecutablePath(ICliController cliController) {
+            var targetDir = CliUtils.GetExecutableTargetDir();
+            var targetPath = Path.Combine(targetDir, cliController.BinaryFileName);
+            return targetPath;
+        }
+        
+        public bool IsDownloaded(ICliController cliController) {
+            return File.Exists(GetExecutablePath(cliController));
+        }
+        
+        public bool CheckIfDownloaded(ICliController cliController) {
+            if(TryUseUserDefinedBinaryPath(cliController, GetExecutablePath(cliController))) {
+                Started = true;
+                Progress = 1f;
+                return true;
+            } else if(IsDownloaded(cliController)) {
+                Started = true;
+                Progress = 1f;
+                return true;
+            } else {
+                Started = false;
+                Progress = 0f;
+                return false;
+            }
+        }
+        
+        public async Task<bool> EnsureDownloaded(ICliController cliController, CancellationToken cancellationToken) {
+            var targetDir = CliUtils.GetExecutableTargetDir();
+            var targetPath = Path.Combine(targetDir, cliController.BinaryFileName);
+            Started = true;
+            if(File.Exists(targetPath)) {
+                Progress = 1f;
+                return true;
+            }
+            Progress = 0f;
+            await ThreadUtility.SwitchToThreadPool(cancellationToken);
+
+            Directory.CreateDirectory(targetDir);
+            if(TryUseUserDefinedBinaryPath(cliController, targetPath)) {
+                Progress = 1f;
+                return true;
+            }
+
+            var tmpPath = CliUtils.GetTempDownloadFilePath("Server.tmp");
+            var attempt = 0;
+            bool sucess = false;
+            HashSet<string> errors = null;
+            while(!sucess) {
+                try {
+                    if (File.Exists(targetPath)) {
+                        Progress = 1f;
+                        return true;
+                    }
+                    // Note: we are writing to temp file so if downloaded file is corrupted it will not cause issues until it's copied to target location
+                    var result = await DownloadUtility.DownloadFile(GetDownloadUrl(cliController), tmpPath, this, cancellationToken).ConfigureAwait(false);
+                    sucess = result.statusCode == HttpStatusCode.OK;
+                } catch (Exception e) {
+                    var error = $"{e.GetType().Name}: {e.Message}";
+                    errors = (errors ?? new HashSet<string>());
+                    if (errors.Add(error)) {
+                        Log.Warning($"Download attempt failed. If the issue persists please reach out to customer support for assistance. Exception: {error}");
+                    }
+                }
+                if (!sucess) {
+                    await Task.Delay(ExponentialBackoff.GetTimeout(attempt), cancellationToken).ConfigureAwait(false);
+                }
+                Progress = 0;
+                attempt++;
+            }
+            
+            if (errors?.Count > 0) {
+                var data = new EditorExtraData {
+                    { StatKey.Errors, new List<string>(errors) },
+                };
+                // sending telemetry requires server to be running so we only attempt after server is downloaded
+                RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Editor, StatEventType.Download), data).Forget();
+                Log.Info("Download succeeded!");
+            }
+            
+            const int ERROR_ALREADY_EXISTS = 0xB7;
+            try {
+                File.Move(tmpPath, targetPath);
+            } catch(IOException ex) when((ex.HResult & 0x0000FFFF) == ERROR_ALREADY_EXISTS) {
+                //another downloader came first
+                try {
+                    File.Delete(tmpPath); 
+                } catch {
+                    //ignored 
+                }
+            }
+            Progress = 1f;
+            return true;
+        }
+
+        static bool TryUseUserDefinedBinaryPath(ICliController cliController, string targetPath) {
+            if (!File.Exists(PackageConst.ConfigFileName)) {
+                return false;
+            } 
+            
+            var config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFileName));
+            var customExecutables = config?.customServerExecutables;
+            if (customExecutables == null) {
+                return false;
+            }
+
+            string customBinaryPath;
+            if(!customExecutables.TryGetValue(cliController.PlatformName, out customBinaryPath)) {
+                return false;
+            }
+            
+            if (!File.Exists(customBinaryPath)) {
+                Log.Warning($"unable to find server binary for platform '{cliController.PlatformName}' at '{customBinaryPath}'. " +
+                            $"Will proceed with downloading the binary (default behavior)");
+                return false;
+            } 
+            
+            try {
+                var targetFile = new FileInfo(targetPath);
+                bool copy = true;
+                if (targetFile.Exists) {
+                    copy = File.GetLastWriteTimeUtc(customBinaryPath) > targetFile.LastWriteTimeUtc;
+                }
+                if (copy) {
+                    Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
+                    File.Copy(customBinaryPath, targetPath, true);
+                }
+                return true;
+            } catch(IOException ex) {
+                Log.Warning("encountered exception when copying server binary in the specified custom executable path '{0}':\n{1}", customBinaryPath, ex);
+                return false;
+            }
+        }
+
+        static string GetDownloadUrl(ICliController cliController) {
+            const string version = PackageConst.ServerVersion;
+            var key = $"{DownloadUtility.GetPackagePrefix(version)}/server/{cliController.PlatformName}/{cliController.BinaryFileName}";
+            return DownloadUtility.GetDownloadUrl(key);
+        }
+
+        void IProgress<float>.Report(float value) {
+            Progress = value;
+        }
+        
+        public Task<bool> PromptForDownload() {
+            if (EditorUtility.DisplayDialog(
+                title: "Install platform specific components",
+                message: InstallDescription,
+                ok: "Install",
+                cancel: "More Info")
+            ) {
+                return EnsureDownloaded(HotReloadCli.controller, CancellationToken.None);
+            }
+            Application.OpenURL(Constants.AdditionalContentURL);
+            return Task.FromResult(false);
+        }
+        
+        public const string InstallDescription = "For Hot Reload to work, additional components specific to your operating system have to be installed";
+    }
+    
+    class DownloadResult {
+        public readonly HttpStatusCode statusCode;
+        public readonly string error;
+        public DownloadResult(HttpStatusCode statusCode, string error) {
+            this.statusCode = statusCode;
+            this.error = error;
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Installation/ServerDownloader.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: f076514e142a4915ab2676a9ca6d884a
+timeCreated: 1676802482

+ 94 - 0
Packages/com.singularitygroup.hotreload/Editor/Installation/UpdateUtility.cs

@@ -0,0 +1,94 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using SingularityGroup.HotReload.Editor.Cli;
+using SingularityGroup.HotReload.RuntimeDependencies;
+using UnityEditor;
+#if UNITY_EDITOR_WIN
+using System.Diagnostics;
+using Debug = UnityEngine.Debug;
+#endif
+
+namespace SingularityGroup.HotReload.Editor {
+    static class UpdateUtility {
+        public static async Task<string> Update(string version, IProgress<float> progress, CancellationToken cancellationToken) {
+            await ThreadUtility.SwitchToThreadPool();
+
+            string serverDir;
+            if(!CliUtils.TryFindServerDir(out serverDir)) {
+                progress?.Report(1);
+                return "unable to locate hot reload package";
+            }
+            var packageDir = Path.GetDirectoryName(Path.GetFullPath(serverDir));
+            var cacheDir = Path.GetFullPath(PackageConst.LibraryCachePath);
+            if(Path.GetPathRoot(packageDir) != Path.GetPathRoot(cacheDir)) {
+                progress?.Report(1);
+                return "unable to update package because it is located on a different drive than the unity project";
+            }
+            var updatedPackageCopy = BackupPackage(packageDir, version);
+            
+            var key = $"{DownloadUtility.GetPackagePrefix(version)}/HotReload.zip";
+            var url = DownloadUtility.GetDownloadUrl(key);
+            var targetFileName = $"HotReload{version.Replace('.', '-')}.zip";
+            var targetFilePath = CliUtils.GetTempDownloadFilePath(targetFileName);
+            var proxy = new Progress<float>(f => progress?.Report(f * 0.7f));
+            var result = await DownloadUtility.DownloadFile(url, targetFilePath, proxy, cancellationToken).ConfigureAwait(false);
+            if(result.error != null) {
+                progress?.Report(1);
+                return result.error;
+            }
+            
+            PackageUpdater.UpdatePackage(targetFilePath, updatedPackageCopy); 
+            progress?.Report(0.8f);
+            
+            var packageRecycleBinDir = PackageConst.LibraryCachePath + $"/PackageArchived-{version}-{Guid.NewGuid():N}";
+            try {
+                Directory.Move(packageDir, packageRecycleBinDir);
+                Directory.Move(updatedPackageCopy, packageDir);
+            } catch {
+                // fallback to replacing files individually if access to the folder is denied
+                PackageUpdater.UpdatePackage(targetFilePath, packageDir); 
+            }
+            try {
+                Directory.Delete(packageRecycleBinDir, true);
+            } catch (IOException) {
+                //ignored
+            }
+            
+            progress?.Report(1);
+            return null;
+        }
+        
+        static string BackupPackage(string packageDir, string version) {
+            var backupPath = PackageConst.LibraryCachePath + $"/PackageBackup-{version}";
+            if(Directory.Exists(backupPath)) {
+                Directory.Delete(backupPath, true);
+            }
+            DirectoryCopy(packageDir, backupPath);
+            return backupPath;
+        }
+        
+        static void DirectoryCopy(string sourceDirPath, string destDirPath) {
+            var rootSource = new DirectoryInfo(sourceDirPath);
+
+            var sourceDirs = rootSource.GetDirectories();
+            // ensure destination directory exists
+            Directory.CreateDirectory(destDirPath);
+
+            // Get the files in the directory and copy them to the new destination
+            var files = rootSource.GetFiles();
+            foreach (var file in files) {
+                string temppath = Path.Combine(destDirPath, file.Name);
+                var copy = file.CopyTo(temppath);
+                copy.LastWriteTimeUtc = file.LastWriteTimeUtc;
+            }
+
+            // copying subdirectories, and their contents to destination
+            foreach (var subdir in sourceDirs) {
+                string subDirDestPath = Path.Combine(destDirPath, subdir.Name);
+                DirectoryCopy(subdir.FullName, subDirDestPath);
+            }
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/Installation/UpdateUtility.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d8485ce38122465e9e70d5992d9ae7ed
+timeCreated: 1676966641

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/PlayerBuild.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0fe483b6b7ad4be79b58901d03e35511
+timeCreated: 1674041345

+ 42 - 0
Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/BuildGenerateBuildInfo.cs

@@ -0,0 +1,42 @@
+using System;
+using System.IO;
+using UnityEditor;
+using UnityEditor.Build;
+
+#pragma warning disable CS0618
+namespace SingularityGroup.HotReload.Editor {
+    public class BuildGenerateBuildInfo : IPreprocessBuild, IPostprocessBuild {
+        public int callbackOrder => 10;
+
+        public void OnPreprocessBuild(BuildTarget target, string path) {
+            try {
+                if (!HotReloadBuildHelper.IncludeInThisBuild()) {
+                    return;
+                }
+                // write BuildInfo json into the StreamingAssets directory
+                GenerateBuildInfo(BuildInfo.GetStoredPath(), target);
+            } catch (BuildFailedException) {
+                throw;
+            } catch (Exception e) {
+                throw new BuildFailedException(e);
+            }
+        }
+        
+        private static void GenerateBuildInfo(string buildFilePath, BuildTarget buildTarget) {
+            var buildInfo = BuildInfoHelper.GenerateBuildInfoMainThread(buildTarget);
+            // write to StreamingAssets
+            // create StreamingAssets folder if not exists (in-case project has no StreamingAssets files)
+            // ReSharper disable once AssignNullToNotNullAttribute
+            Directory.CreateDirectory(Path.GetDirectoryName(buildFilePath));
+            File.WriteAllText(buildFilePath, buildInfo.ToJson());
+        }
+        
+        public void OnPostprocessBuild(BuildTarget target, string path) {
+            try {
+                File.Delete(BuildInfo.GetStoredPath());
+            } catch {
+                // ignore 
+            }
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/BuildGenerateBuildInfo.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 178df48ca88b4cddac448a49196b49bf
+timeCreated: 1682338738

+ 111 - 0
Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/HotReloadBuildHelper.cs

@@ -0,0 +1,111 @@
+using System;
+using System.IO;
+using UnityEditor;
+using UnityEditor.Build;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+    internal static class HotReloadBuildHelper {
+        /// <summary>
+        /// Should HotReload runtime be included in the current build?
+        /// </summary>
+        public static bool IncludeInThisBuild() {
+            return IsAllBuildSettingsSupported();
+        }
+
+        /// <summary>
+        /// Get scripting backend for the current platform.
+        /// </summary>
+        /// <returns>Scripting backend</returns>
+        public static ScriptingImplementation GetCurrentScriptingBackend() {
+#pragma warning disable CS0618
+            return PlayerSettings.GetScriptingBackend(BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget));
+#pragma warning restore CS0618
+        }
+
+        public static ManagedStrippingLevel GetCurrentStrippingLevel() {
+#pragma warning disable CS0618
+            return PlayerSettings.GetManagedStrippingLevel(BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget));
+#pragma warning restore CS0618
+        }
+
+        public static void SetCurrentScriptingBackend(ScriptingImplementation to) {
+#pragma warning disable CS0618
+            // only set it if default is not correct (avoid changing ProjectSettings when not needed)
+            if (GetCurrentScriptingBackend() != to) {
+                PlayerSettings.SetScriptingBackend(EditorUserBuildSettings.selectedBuildTargetGroup, to);
+            }
+#pragma warning restore CS0618
+        }
+        
+        public static void SetCurrentStrippingLevel(ManagedStrippingLevel to) {
+#pragma warning disable CS0618
+            // only set it if default is not correct (avoid changing ProjectSettings when not needed)
+            if (GetCurrentStrippingLevel() != to) {
+                PlayerSettings.SetManagedStrippingLevel(EditorUserBuildSettings.selectedBuildTargetGroup, to);
+            }
+#pragma warning restore CS0618
+        }
+
+        /// Is the current build target supported?
+        /// main thread only
+        public static bool IsBuildTargetSupported() {
+            var buildTarget = EditorUserBuildSettings.selectedBuildTargetGroup;  
+            return Array.IndexOf(unsupportedBuildTargets, buildTarget) == -1;
+        }
+        
+        /// Are all the settings supported?
+        /// main thread only
+        static bool IsAllBuildSettingsSupported() {
+            if (!IsBuildTargetSupported()) {
+                return false;
+            }
+
+            // need way to give it settings object, dont want to give serializedobject
+            var options = HotReloadSettingsEditor.LoadSettingsOrDefault();
+            var so = new SerializedObject(options);
+            
+            // check all projeect options
+            foreach (var option in HotReloadSettingsTab.allOptions) {
+                var projectOption = option as ProjectOptionBase;
+                if (projectOption != null) {
+                    // if option is required, build can't use hot reload
+                    if (projectOption.IsRequiredForBuild() && !projectOption.GetValue(so)) {
+                        return false;
+                    }
+                }
+            }
+
+            return GetCurrentScriptingBackend() == ScriptingImplementation.Mono2x
+                && GetCurrentStrippingLevel() == ManagedStrippingLevel.Disabled
+                && EditorUserBuildSettings.development;
+        }
+
+        /// <summary>
+        /// Some platforms are not supported because they don't have Mono scripting backend.
+        /// </summary>
+        /// <remarks>
+        /// Only list the platforms that definately don't have Mono scripting.
+        /// </remarks>
+        private static readonly BuildTargetGroup[] unsupportedBuildTargets = new [] {
+            BuildTargetGroup.iOS, // mono support was removed many years ago
+            BuildTargetGroup.WebGL, // has never had mono
+        };
+        
+#pragma warning disable CS0618
+        public static bool IsMonoSupported(BuildTargetGroup buildTarget) {
+            var backend = PlayerSettings.GetScriptingBackend(buildTarget);
+            try {
+                // GetDefaultScriptingBackend returns IL2CPP for Unity 6 which goes against Unity documentation.
+                // Have to use a workaround approach instead
+                PlayerSettings.SetScriptingBackend(buildTarget, ScriptingImplementation.Mono2x);
+                return PlayerSettings.GetScriptingBackend(buildTarget) == ScriptingImplementation.Mono2x;
+            } catch {
+                return false;
+            } finally {
+                PlayerSettings.SetScriptingBackend(buildTarget, backend);
+            }
+        }
+#pragma warning restore CS0618
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/HotReloadBuildHelper.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b9aa611f02544b609c5b29f9d1409d6e
+timeCreated: 1674041425

+ 133 - 0
Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/PostbuildModifyAndroidManifest.cs

@@ -0,0 +1,133 @@
+using System;
+using System.IO;
+using System.Text.RegularExpressions;
+using UnityEditor.Android;
+using UnityEditor.Build;
+
+namespace SingularityGroup.HotReload.Editor {
+#pragma warning disable CS0618
+    /// <remarks>
+    /// <para>
+    /// This class sets option in the AndroidManifest that you choose in HotReload build settings.
+    /// </para>
+    /// <para>
+    /// - To connect to the HotReload server through the local network, we need to permit access to http://192...<br/>
+    /// - Starting with Android 9, insecure http requests are not allowed by default and must be whitelisted
+    /// </para>
+    /// </remarks>
+    internal class PostbuildModifyAndroidManifest : IPostGenerateGradleAndroidProject {
+#pragma warning restore CS0618
+        public int callbackOrder => 10;
+
+        private const string manifestFileName = "AndroidManifest.xml";
+
+        public void OnPostGenerateGradleAndroidProject(string path) {
+            try {
+                if (!HotReloadBuildHelper.IncludeInThisBuild()) {
+                    return;
+                }
+                // Note: in future we may support users with custom configuration for usesCleartextTraffic
+                #if UNITY_2022_1_OR_NEWER
+                // Unity 2022 or newer → do nothing, we rely on Unity option to control the flag
+                #else
+                // Unity 2021 or older → put manifest flag in if Unity is making a Development Build
+                var manifestFilePath = FindAndroidManifest(path);
+                if (manifestFilePath == null) {
+                    throw new BuildFailedException($"[{CodePatcher.TAG}] Unable to find {manifestFileName}");
+                }
+                SetUsesCleartextTraffic(manifestFilePath);
+                #endif
+            } catch (BuildFailedException) {
+                throw;
+            } catch (Exception e) {
+                throw new BuildFailedException(e);
+            }
+        }
+
+        /// identifier that is used in the deeplink uri scheme
+        /// (initially tried Application.identifier, but that was giving unexpected results based on PlayerSettings)
+        //  SG-29580
+        //  Something to uniqly identify the application, but it must be something which is highly likely
+        //  to be the same at build time (studio might have logic to set e.g. product name to MyGameProd or MyGameTest)
+        public static string ApplicationIdentiferSlug => "app";
+/*
+        public static string ApplicationIdentiferSlug => Regex.Replace(ApplicationIdentifer, @"[^a-zA-Z0-9\.\-]", "")
+            .Replace("..", ".") // happens if your companyname in Unity ends with a dot
+            .ToLowerInvariant();
+
+        private static void AddDeeplinkForwarder(string manifestFilePath) {
+            // add the hotreload-${identifier} uri scheme to the AndroidManifest.xml file
+            // it should be added as part of an intent-filter for the activity "com.singularitygroup.deeplinkforwarder.DeepLinkForwarderActivity"
+            var contents = File.ReadAllText(manifestFilePath);
+            if (contents.Contains("android:name=\"com.singularitygroup.deeplinkforwarder.DeepLinkForwarderActivity\"")) {
+                // user has already set this themselves, don't replace it
+                return;
+            }
+
+            //note: not using android:host or any other data attr because android still shows a chooser for all ur hotreload apps
+            // Therefore must use a unique uri scheme to ensure only one app can handle it.
+            var activityWithIntentFilter = @"
+<activity android:name=""com.singularitygroup.deeplinkforwarder.DeepLinkForwarderActivity"">
+    <intent-filter>
+        <action android:name=""android.intent.action.VIEW"" />
+        <category android:name=""android.intent.category.DEFAULT"" />
+        <category android:name=""android.intent.category.BROWSABLE"" />
+        <data android:scheme=""hotreload-" + ApplicationIdentiferSlug + @""" />
+    </intent-filter>
+</activity>";
+            var newContents = Regex.Replace(contents,
+                @"</application>",
+                activityWithIntentFilter + "\n    </application>"
+            );
+            File.WriteAllText(manifestFilePath, newContents);
+        }
+*/
+        // Assume unityLibraryPath is to {gradleProject}/unityLibrary/ which is roughly the same across Unity versions 2018/2019/2020/2021/2022
+        private static string FindAndroidManifest(string unityLibraryPath) {
+            // find the AndroidManifest.xml file which we can edit
+            var dir = new DirectoryInfo(unityLibraryPath);
+            var manifestFilePath = Path.Combine(dir.FullName, "src", "main", manifestFileName);
+            if (File.Exists(manifestFilePath)) {
+                return manifestFilePath;
+            }
+
+            Log.Info("Did not find {0} at {1}, searching for manifest file inside {2}", manifestFileName, manifestFilePath, dir.FullName);
+            var manifestFiles = dir.GetFiles(manifestFileName, SearchOption.AllDirectories);
+            if (manifestFiles.Length == 0) {
+                return null;
+            }
+
+            foreach (var file in manifestFiles) {
+                if (file.FullName.Contains("src")) {
+                    // good choice
+                    return file.FullName;
+                }
+            }
+            // fallback to the first file found
+            return manifestFiles[0].FullName;
+        }
+
+        /// <summary>
+        /// Set option android:usesCleartextTraffic="true"
+
+        /// </summary>
+        /// <param name="manifestFilePath">Absolute filepath to the unityLibrary AndroidManifest.xml file</param>
+        private static void SetUsesCleartextTraffic(string manifestFilePath) {
+            // Ideally we would create or modify a "Network Security Configuration file" to permit access to local ip addresses
+            // https://developer.android.com/training/articles/security-config#manifest
+            // but that becomes difficult when the user has their own configuration file - would need to search for it and it may be inside an aar.
+            var contents = File.ReadAllText(manifestFilePath);
+            if (contents.Contains("android:usesCleartextTraffic=")) {
+                // user has already set this themselves, don't replace it
+                return;
+            }
+            var newContents = Regex.Replace(contents,
+                @"<application\s",
+                "<application android:usesCleartextTraffic=\"true\" "
+            );
+            newContents += $"\n<!-- [{CodePatcher.TAG}] Added android:usesCleartextTraffic=\"true\" to permit connecting to the Hot Reload http server running on your machine. -->";
+            newContents += $"\n<!-- [{CodePatcher.TAG}] This change only happens in Unity development builds. You can disable this in the Hot Reload settings window. -->";
+            File.WriteAllText(manifestFilePath, newContents);
+        }
+    }
+}

+ 3 - 0
Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/PostbuildModifyAndroidManifest.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 1949292efc07445ea4c040d544e2d369
+timeCreated: 1675441886

+ 26 - 0
Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/PostbuildSendProjectState.cs

@@ -0,0 +1,26 @@
+using System;
+using SingularityGroup.HotReload.Editor.Cli;
+using UnityEditor;
+using UnityEditor.Build;
+
+namespace SingularityGroup.HotReload.Editor {
+#pragma warning disable CS0618
+    class PostbuildSendProjectState : IPostprocessBuild {
+#pragma warning restore CS0618
+        public int callbackOrder => 9999;
+        public void OnPostprocessBuild(BuildTarget target, string path) {
+            try {
+                if (!HotReloadBuildHelper.IncludeInThisBuild()) {
+                    return;
+                }
+                // after build passes, need to send again because EditorApplication.delayCall isn't called.
+                var buildInfo = BuildInfoHelper.GenerateBuildInfoMainThread();
+                HotReloadCli.PrepareBuildInfo(buildInfo);
+            } catch (BuildFailedException) {
+                throw;
+            } catch (Exception e) {
+                throw new BuildFailedException(e);
+            }
+        }
+    }
+}

+ 11 - 0
Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/PostbuildSendProjectState.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3b27b9eab16f78f448477e546fd5eb97
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 60 - 0
Packages/com.singularitygroup.hotreload/Editor/PlayerBuild/PrebuildIncludeResources.cs

@@ -0,0 +1,60 @@
+using System;
+using UnityEditor;
+using UnityEditor.Build;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+    /// <summary>Includes HotReload Resources only in development builds</summary>
+    /// <remarks>
+    /// This build script ensures that HotReload Resources are not included in release builds.
+    /// <para>
+    /// When HotReload is enabled:<br/>
+    ///   - include HotReloadSettingsObject in development Android builds.<br/>
+    ///   - exclude HotReloadSettingsObject from the build.<br/>
+    /// When HotReload is disabled:<br/>
+    ///   - excludes HotReloadSettingsObject from the build.<br/>
+    /// </para>
+    /// </remarks>
+#pragma warning disable CS0618
+    internal class PrebuildIncludeResources : IPreprocessBuild, IPostprocessBuild {
+#pragma warning restore CS0618
+        public int callbackOrder => 10;
+
+        // Preprocess warnings don't show up in console
+        bool warnSettingsNotSupported;
+        
+        public void OnPreprocessBuild(BuildTarget target, string path) {
+            try {
+                if (HotReloadBuildHelper.IncludeInThisBuild()) {
+                    // move scriptable object into Resources/ folder
+                    HotReloadSettingsEditor.AddOrRemoveFromBuild(true);
+                } else {
+                    // make sure HotReload resources are not in the build
+                    HotReloadSettingsEditor.AddOrRemoveFromBuild(false);
+                    
+                    var options = HotReloadSettingsEditor.LoadSettingsOrDefault();
+                    var so = new SerializedObject(options);
+                    if (IncludeInBuildOption.I.GetValue(so)) {
+                        warnSettingsNotSupported = true;
+                    }
+                }
+            } catch (BuildFailedException) {
+                throw;
+            } catch (Exception ex) {
+                throw new BuildFailedException(ex);
+            }
+        }
+        
+        public void OnPostprocessBuild(BuildTarget target, string path) {
+            if (warnSettingsNotSupported) {
+                Debug.LogWarning("Hot Reload was not included in the build because one or more build settings were not supported.");
+            }
+        }
+
+        // Do nothing in post build. settings asset will be dirty if build fails, so not worth fixing just for successful builds.
+        // [PostProcessBuild]
+        // private static void PostBuild(BuildTarget target, string pathToBuiltProject) {
+        // }
+    }
+
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов