EditorCodePatcher.cs 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using JetBrains.Annotations;
  9. using SingularityGroup.HotReload.DTO;
  10. using SingularityGroup.HotReload.Editor.Cli;
  11. using SingularityGroup.HotReload.Editor.Demo;
  12. using SingularityGroup.HotReload.EditorDependencies;
  13. using SingularityGroup.HotReload.RuntimeDependencies;
  14. using UnityEditor;
  15. using UnityEngine;
  16. using Debug = UnityEngine.Debug;
  17. using Task = System.Threading.Tasks.Task;
  18. using System.Reflection;
  19. using System.Runtime.CompilerServices;
  20. using SingularityGroup.HotReload.Newtonsoft.Json;
  21. using SingularityGroup.HotReload.ZXing;
  22. using UnityEditor.Compilation;
  23. using UnityEditor.UIElements;
  24. using UnityEditorInternal;
  25. using UnityEngine.UIElements;
  26. [assembly: InternalsVisibleTo("SingularityGroup.HotReload.IntegrationTests")]
  27. namespace SingularityGroup.HotReload.Editor {
  28. internal class Config {
  29. public bool patchEditModeOnlyOnEditorFocus;
  30. public string[] assetBlacklist;
  31. public bool changePlaymodeTint;
  32. public bool disableCompilingFromEditorScripts;
  33. public bool enableInspectorFreezeFix;
  34. }
  35. [InitializeOnLoad]
  36. internal static class EditorCodePatcher {
  37. const string sessionFilePath = PackageConst.LibraryCachePath + "/sessionId.txt";
  38. const string patchesFilePath = PackageConst.LibraryCachePath + "/patches.json";
  39. internal static readonly ServerDownloader serverDownloader;
  40. internal static bool _compileError;
  41. internal static bool _applyingFailed;
  42. internal static bool _appliedPartially;
  43. internal static bool _appliedUndetected;
  44. static Timer timer;
  45. static bool init;
  46. internal static UnityLicenseType licenseType { get; private set; }
  47. internal static bool LoginNotRequired => PackageConst.IsAssetStoreBuild && licenseType != UnityLicenseType.UnityPro;
  48. internal static bool compileError => _compileError;
  49. internal static PatchStatus patchStatus = PatchStatus.None;
  50. internal static event Action<(MethodPatchResponse, RegisterPatchesResult)> OnPatchHandled;
  51. internal static Config config;
  52. #if ODIN_INSPECTOR
  53. internal static bool DrawPrefix(Sirenix.OdinInspector.Editor.InspectorProperty __instance) {
  54. return !UnityFieldHelper.IsFieldHidden(__instance.ParentType, __instance.Name);
  55. }
  56. internal static MethodInfo OdinPropertyDrawPrefixInfo = typeof(EditorCodePatcher).GetMethod("DrawPrefix", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
  57. #if UNITY_2021_1_OR_NEWER
  58. internal static MethodInfo OdinPropertyDrawInfo = typeof(Sirenix.OdinInspector.Editor.InspectorProperty)?.GetMethod("Draw", 0, BindingFlags.Instance | BindingFlags.Public, null, new Type[]{}, null);
  59. #else
  60. internal static MethodInfo OdinPropertyDrawInfo = typeof(Sirenix.OdinInspector.Editor.InspectorProperty)?.GetMethod("Draw", BindingFlags.Instance | BindingFlags.Public, null, new Type[]{}, null);
  61. #endif
  62. internal static MethodInfo DrawOdinInspectorInfo = typeof(Sirenix.OdinInspector.Editor.OdinEditor)?.GetMethod("DrawOdinInspector", BindingFlags.NonPublic | BindingFlags.Instance);
  63. #else
  64. internal static MethodInfo OdinPropertyDrawPrefixInfo = null;
  65. internal static MethodInfo OdinPropertyDrawInfo = null;
  66. internal static MethodInfo DrawOdinInspectorInfo = null;
  67. #endif
  68. internal static MethodInfo GetDrawVInspectorInfo() {
  69. // performance optimization
  70. if (!Directory.Exists("Assets/vInspector")) {
  71. return null;
  72. }
  73. try {
  74. var t = Type.GetType("VInspector.AbstractEditor, VInspector");
  75. return t?.GetMethod("OnInspectorGUI", BindingFlags.Public | BindingFlags.Instance);
  76. } catch {
  77. // ignore
  78. }
  79. return null;
  80. }
  81. internal static ICompileChecker compileChecker;
  82. static bool quitting;
  83. static EditorCodePatcher() {
  84. if(init) {
  85. //Avoid infinite recursion in case the static constructor gets accessed via `InitPatchesBlocked` below
  86. return;
  87. }
  88. if (File.Exists(PackageConst.ConfigFileName)) {
  89. config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFileName));
  90. } else {
  91. config = new Config();
  92. }
  93. init = true;
  94. UnityHelper.Init();
  95. //Use synchonization context if possible because it's more reliable.
  96. ThreadUtility.InitEditor();
  97. if (!EditorWindowHelper.IsHumanControllingUs()) {
  98. return;
  99. }
  100. serverDownloader = new ServerDownloader();
  101. serverDownloader.CheckIfDownloaded(HotReloadCli.controller);
  102. SingularityGroup.HotReload.Demo.Demo.I = new EditorDemo();
  103. if (HotReloadPrefs.DeactivateHotReload || new DirectoryInfo(Path.GetFullPath("..")).Name == "VP") {
  104. ResetSettings();
  105. return;
  106. }
  107. // ReSharper disable ExpressionIsAlwaysNull
  108. UnityFieldHelper.Init(Log.Warning, HotReloadRunTab.Recompile, DrawOdinInspectorInfo, OdinPropertyDrawInfo, OdinPropertyDrawPrefixInfo, GetDrawVInspectorInfo(), typeof(UnityFieldDrawerPatchHelper));
  109. timer = new Timer(OnIntervalThreaded, (Action) OnIntervalMainThread, 500, 500);
  110. UpdateHost();
  111. licenseType = UnityLicenseHelper.GetLicenseType();
  112. compileChecker = CompileChecker.Create();
  113. compileChecker.onCompilationFinished += OnCompilationFinished;
  114. EditorApplication.delayCall += InstallUtility.CheckForNewInstall;
  115. AddEditorFocusChangedHandler(OnEditorFocusChanged);
  116. // When domain reloads, this is a good time to ensure server has up-to-date project information
  117. if (ServerHealthCheck.I.IsServerHealthy) {
  118. EditorApplication.delayCall += TryPrepareBuildInfo;
  119. }
  120. HotReloadSuggestionsHelper.Init();
  121. // reset in case last session didn't shut down properly
  122. CheckEditorSettings();
  123. EditorApplication.quitting += ResetSettingsOnQuit;
  124. AssemblyReloadEvents.beforeAssemblyReload += () => {
  125. HotReloadTimelineHelper.PersistTimeline();
  126. };
  127. CompilationPipeline.compilationFinished += obj => {
  128. // reset in case package got removed
  129. // if it got removed, it will not be enabled again
  130. // if it wasn't removed, settings will get handled by OnIntervalMainThread
  131. AutoRefreshSettingChecker.Reset();
  132. ScriptCompilationSettingChecker.Reset();
  133. PlaymodeTintSettingChecker.Reset();
  134. HotReloadRunTab.recompiling = false;
  135. CompileMethodDetourer.Reset();
  136. };
  137. DetectEditorStart();
  138. DetectVersionUpdate();
  139. CodePatcher.I.fieldHandler = new FieldHandler(FieldDrawerUtil.StoreField, UnityFieldHelper.HideField, UnityFieldHelper.RegisterInspectorFieldAttributes);
  140. if (EditorApplication.isPlayingOrWillChangePlaymode) {
  141. CodePatcher.I.InitPatchesBlocked(patchesFilePath);
  142. HotReloadTimelineHelper.InitPersistedEvents();
  143. }
  144. #pragma warning disable CS0612 // Type or member is obsolete
  145. if (HotReloadPrefs.RateAppShownLegacy) {
  146. HotReloadPrefs.RateAppShown = true;
  147. }
  148. if (!File.Exists(HotReloadPrefs.showOnStartupPath)) {
  149. var showOnStartupLegacy = HotReloadPrefs.GetShowOnStartupEnum();
  150. HotReloadPrefs.ShowOnStartup = showOnStartupLegacy;
  151. }
  152. #pragma warning restore CS0612 // Type or member is obsolete
  153. HotReloadState.ShowingRedDot = false;
  154. if (DateTime.Now < new DateTime(2023, 11, 1)) {
  155. HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023);
  156. } else {
  157. HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023);
  158. }
  159. EditorApplication.playModeStateChanged += state => {
  160. if (state == PlayModeStateChange.EnteredEditMode && HotReloadPrefs.AutoRecompileUnsupportedChangesOnExitPlayMode) {
  161. if (TryRecompileUnsupportedChanges()) {
  162. HotReloadState.RecompiledUnsupportedChangesOnExitPlaymode = true;
  163. }
  164. }
  165. };
  166. if (HotReloadState.RecompiledUnsupportedChangesInPlaymode) {
  167. HotReloadState.RecompiledUnsupportedChangesInPlaymode = false;
  168. EditorApplication.isPlaying = true;
  169. }
  170. #if UNITY_2020_1_OR_NEWER
  171. if (CompilationPipeline.codeOptimization != CodeOptimization.Release) {
  172. HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods);
  173. }
  174. #endif
  175. if (!HotReloadState.EditorCodePatcherInit) {
  176. ClearPersistence();
  177. HotReloadState.EditorCodePatcherInit = true;
  178. }
  179. CodePatcher.I.debuggerCompatibilityEnabled = !HotReloadPrefs.AutoDisableHotReloadWithDebugger;
  180. }
  181. static void ResetSettingsOnQuit() {
  182. quitting = true;
  183. ResetSettings();
  184. }
  185. static void ResetSettings() {
  186. AutoRefreshSettingChecker.Reset();
  187. ScriptCompilationSettingChecker.Reset();
  188. PlaymodeTintSettingChecker.Reset();
  189. HotReloadCli.StopAsync().Forget();
  190. CompileMethodDetourer.Reset();
  191. }
  192. public static bool autoRecompileUnsupportedChangesSupported;
  193. static void AddEditorFocusChangedHandler(Action<bool> handler) {
  194. var eventInfo = typeof(EditorApplication).GetEvent("focusChanged", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
  195. var addMethod = eventInfo?.GetAddMethod(true) ?? eventInfo?.GetAddMethod(false);
  196. if (addMethod != null) {
  197. addMethod.Invoke(null, new object[]{ handler });
  198. }
  199. autoRecompileUnsupportedChangesSupported = addMethod != null;
  200. }
  201. private static void OnEditorFocusChanged(bool hasFocus) {
  202. if (hasFocus && !HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately) {
  203. TryRecompileUnsupportedChanges();
  204. }
  205. }
  206. public static bool TryRecompileUnsupportedChanges() {
  207. var isPlaying = EditorApplication.isPlaying;
  208. if (!HotReloadPrefs.AutoRecompileUnsupportedChanges
  209. || HotReloadTimelineHelper.UnsupportedChangesCount == 0
  210. && (!HotReloadPrefs.AutoRecompilePartiallyUnsupportedChanges || HotReloadTimelineHelper.PartiallySupportedChangesCount == 0)
  211. || _compileError
  212. || isPlaying && !HotReloadPrefs.AutoRecompileUnsupportedChangesInPlayMode
  213. ) {
  214. return false;
  215. }
  216. if (HotReloadPrefs.ShowCompilingUnsupportedNotifications) {
  217. EditorWindowHelper.ShowNotification(EditorWindowHelper.NotificationStatus.NeedsRecompile);
  218. }
  219. if (isPlaying) {
  220. HotReloadState.RecompiledUnsupportedChangesInPlaymode = true;
  221. }
  222. HotReloadRunTab.Recompile();
  223. return true;
  224. }
  225. private static DateTime lastPrepareBuildInfo = DateTime.UtcNow;
  226. /// Post state for player builds.
  227. /// Only check build target because user can change build settings whenever.
  228. internal static void TryPrepareBuildInfo() {
  229. // Note: we post files state even when build target is wrong
  230. // because you might connect with a build downloaded onto the device.
  231. if ((DateTime.UtcNow - lastPrepareBuildInfo).TotalSeconds > 5) {
  232. lastPrepareBuildInfo = DateTime.UtcNow;
  233. HotReloadCli.PrepareBuildInfoAsync().Forget();
  234. }
  235. }
  236. internal static void RecordActiveDaysForRateApp() {
  237. var unixDay = (int)(DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 86400);
  238. var activeDays = GetActiveDaysForRateApp();
  239. if (activeDays.Count < Constants.DaysToRateApp && activeDays.Add(unixDay.ToString())) {
  240. HotReloadPrefs.ActiveDays = string.Join(",", activeDays);
  241. }
  242. }
  243. internal static HashSet<string> GetActiveDaysForRateApp() {
  244. if (string.IsNullOrEmpty(HotReloadPrefs.ActiveDays)) {
  245. return new HashSet<string>();
  246. }
  247. return new HashSet<string>(HotReloadPrefs.ActiveDays.Split(','));
  248. }
  249. // CheckEditorStart distinguishes between domain reload and first editor open
  250. // We have some separate logic on editor start (InstallUtility.HandleEditorStart)
  251. private static void DetectEditorStart() {
  252. var editorId = EditorAnalyticsSessionInfo.id;
  253. var currVersion = PackageConst.Version;
  254. Task.Run(() => {
  255. try {
  256. var lines = File.Exists(sessionFilePath) ? File.ReadAllLines(sessionFilePath) : Array.Empty<string>();
  257. long prevSessionId = -1;
  258. string prevVersion = null;
  259. if (lines.Length >= 2) {
  260. long.TryParse(lines[1], out prevSessionId);
  261. }
  262. if (lines.Length >= 3) {
  263. prevVersion = lines[2].Trim();
  264. }
  265. var updatedFromVersion = (prevSessionId != -1 && currVersion != prevVersion) ? prevVersion : null;
  266. if (prevSessionId != editorId && prevSessionId != 0) {
  267. // back to mainthread
  268. ThreadUtility.RunOnMainThread(() => {
  269. InstallUtility.HandleEditorStart(updatedFromVersion);
  270. var newEditorId = EditorAnalyticsSessionInfo.id;
  271. if (newEditorId != 0) {
  272. Task.Run(() => {
  273. try {
  274. // editorId isn't available on first domain reload, must do it here
  275. File.WriteAllLines(sessionFilePath, new[] {
  276. "1", // serialization version
  277. newEditorId.ToString(),
  278. currVersion,
  279. });
  280. } catch (IOException) {
  281. // ignore
  282. }
  283. });
  284. }
  285. });
  286. }
  287. } catch (IOException) {
  288. // ignore
  289. } catch (Exception e) {
  290. ThreadUtility.LogException(e);
  291. }
  292. });
  293. }
  294. private static void DetectVersionUpdate() {
  295. if (serverDownloader.CheckIfDownloaded(HotReloadCli.controller)) {
  296. return;
  297. }
  298. ServerHealthCheck.instance.CheckHealth();
  299. if (!ServerHealthCheck.I.IsServerHealthy) {
  300. return;
  301. }
  302. var restartServer = EditorUtility.DisplayDialog("Hot Reload",
  303. $"When updating Hot Reload, the server must be restarted for the update to take effect." +
  304. "\nDo you want to restart it now?",
  305. "Restart server", "Don't restart");
  306. if (restartServer) {
  307. RestartCodePatcher().Forget();
  308. }
  309. }
  310. private static void UpdateHost() {
  311. RequestHelper.SetServerInfo(new PatchServerInfo(RequestHelper.defaultServerHost, HotReloadState.ServerPort, null, Path.GetFullPath(".")));
  312. }
  313. static void OnIntervalThreaded(object o) {
  314. ServerHealthCheck.instance.CheckHealth();
  315. ThreadUtility.RunOnMainThread((Action)o);
  316. if (serverDownloader.Progress >= 1f) {
  317. serverDownloader.CheckIfDownloaded(HotReloadCli.controller);
  318. }
  319. }
  320. private static bool _requestingFlushErrors;
  321. private static long _lastErrorFlush;
  322. private static async Task RequestFlushErrors() {
  323. _requestingFlushErrors = true;
  324. try {
  325. await RequestFlushErrorsCore();
  326. } finally {
  327. _requestingFlushErrors = false;
  328. }
  329. }
  330. private static async Task RequestFlushErrorsCore() {
  331. var pollFrequency = 500;
  332. // Delay until we've hit the poll request frequency
  333. var waitMs = (int)Mathf.Clamp(pollFrequency - ((DateTime.Now.Ticks / (float)TimeSpan.TicksPerMillisecond) - _lastErrorFlush), 0, pollFrequency);
  334. await Task.Delay(waitMs);
  335. await FlushErrors();
  336. _lastErrorFlush = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
  337. }
  338. public static bool disableServerLogs;
  339. public static string lastCompileErrorLog;
  340. static async Task FlushErrors() {
  341. var response = await RequestHelper.RequestFlushErrors();
  342. if (response == null || disableServerLogs) {
  343. return;
  344. }
  345. foreach (var responseWarning in response.warnings) {
  346. if (responseWarning.Contains("Scripts have compile errors")) {
  347. if (compileError) {
  348. Log.Error(responseWarning);
  349. } else {
  350. lastCompileErrorLog = responseWarning;
  351. }
  352. } else {
  353. Log.Warning(responseWarning);
  354. }
  355. if (responseWarning.Contains("Multidimensional arrays are not supported")) {
  356. await ThreadUtility.SwitchToMainThread();
  357. HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.MultidimensionalArrays);
  358. }
  359. }
  360. foreach (var responseError in response.errors) {
  361. Log.Error(responseError);
  362. }
  363. }
  364. internal static bool firstPatchAttempted;
  365. internal static bool loggedDebuggerRecompile;
  366. static void OnIntervalMainThread() {
  367. HotReloadSuggestionsHelper.Check();
  368. // Moved from RequestServerInfo to avoid GC allocations when HR is not active
  369. // Repaint if the running Status has changed since the layout changes quite a bit
  370. if (running != ServerHealthCheck.I.IsServerHealthy) {
  371. if (HotReloadWindow.Current) {
  372. HotReloadRunTab.RepaintInstant();
  373. }
  374. running = ServerHealthCheck.I.IsServerHealthy;
  375. }
  376. if (!running) {
  377. startupCompletedAt = null;
  378. }
  379. if (!running && !StartedServerRecently()) {
  380. // Reset startup progress
  381. startupProgress = null;
  382. }
  383. if (HotReloadPrefs.AutoDisableHotReloadWithDebugger && Debugger.IsAttached) {
  384. if (!HotReloadState.ShowedDebuggerCompatibility) {
  385. HotReloadSuggestionsHelper.SetSuggestionActive(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
  386. HotReloadState.ShowedDebuggerCompatibility = true;
  387. }
  388. if (CodePatcher.I.OriginalPatchMethods.Count() > 0) {
  389. if (!Application.isPlaying) {
  390. if (!loggedDebuggerRecompile) {
  391. Log.Info("Debugger was attached. Hot Reload may interfere with your debugger session. Recompiling in order to get full debugger experience.");
  392. loggedDebuggerRecompile = true;
  393. }
  394. HotReloadRunTab.Recompile();
  395. HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached);
  396. } else {
  397. HotReloadSuggestionsHelper.SetSuggestionActive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached);
  398. }
  399. }
  400. } else if (HotReloadSuggestionsHelper.CheckSuggestionActive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached)) {
  401. HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached);
  402. }
  403. if(ServerHealthCheck.I.IsServerHealthy) {
  404. // NOTE: avoid calling this method when HR is not running to avoid allocations
  405. RequestServerInfo();
  406. TryPrepareBuildInfo();
  407. if (!requestingCompile && (!config.patchEditModeOnlyOnEditorFocus || Application.isPlaying || UnityEditorInternal.InternalEditorUtility.isApplicationActive)) {
  408. RequestHelper.PollMethodPatches(HotReloadState.LastPatchId, resp => HandleResponseReceived(resp));
  409. }
  410. RequestHelper.PollPatchStatus(resp => {
  411. patchStatus = resp.patchStatus;
  412. if (patchStatus == PatchStatus.Compiling) {
  413. startWaitingForCompile = null;
  414. }
  415. if (patchStatus == PatchStatus.Patching) {
  416. firstPatchAttempted = true;
  417. if (HotReloadPrefs.ShowPatchingNotifications) {
  418. EditorWindowHelper.ShowNotification(EditorWindowHelper.NotificationStatus.Patching, maxDuration: 10);
  419. }
  420. } else if (HotReloadPrefs.ShowPatchingNotifications) {
  421. EditorWindowHelper.RemoveNotification();
  422. }
  423. }, patchStatus);
  424. if (HotReloadPrefs.AllAssetChanges) {
  425. RequestHelper.PollAssetChanges(HandleAssetChange);
  426. }
  427. #if UNITY_2020_1_OR_NEWER
  428. if (!disableInlineChecks) {
  429. CheckInlinedMethods();
  430. }
  431. #endif
  432. }
  433. if (!ServerHealthCheck.I.IsServerHealthy) {
  434. stopping = false;
  435. }
  436. if (startupProgress?.Item1 == 1) {
  437. starting = false;
  438. }
  439. if (!_requestingFlushErrors && Running) {
  440. RequestFlushErrors().Forget();
  441. }
  442. CheckEditorSettings();
  443. }
  444. #if UNITY_2020_1_OR_NEWER
  445. //only disabled for integration tests
  446. internal static bool disableInlineChecks = false;
  447. internal static HashSet<MethodBase> inlinedMethodsFound = new HashSet<MethodBase>();
  448. internal static void CheckInlinedMethods() {
  449. if (CompilationPipeline.codeOptimization != CodeOptimization.Release) {
  450. return;
  451. }
  452. HashSet<MethodBase> newInlinedMethods = null;
  453. try {
  454. foreach (var method in CodePatcher.I.OriginalPatchMethods) {
  455. if (inlinedMethodsFound.Contains(method)) {
  456. continue;
  457. }
  458. var isMethodSynthesized = method.Name.Contains("<") || method.DeclaringType?.Name.Contains("<") == true && method.Name == ".ctor";
  459. if (!(method is ConstructorInfo) && !isMethodSynthesized && MethodUtils.IsMethodInlined(method)) {
  460. if (newInlinedMethods == null) {
  461. newInlinedMethods = new HashSet<MethodBase>();
  462. }
  463. newInlinedMethods.Add(method);
  464. }
  465. }
  466. if (newInlinedMethods?.Count > 0) {
  467. if (!HotReloadPrefs.LoggedInlinedMethodsDialogue) {
  468. 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.");
  469. HotReloadPrefs.LoggedInlinedMethodsDialogue = true;
  470. }
  471. HotReloadTimelineHelper.CreateInlinedMethodsEntry(entryType: EntryType.Foldout, patchedMethodsDisplayNames: newInlinedMethods.Select(mb => $"{mb.DeclaringType?.Name}::{mb.Name}").ToArray());
  472. if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) {
  473. TryRecompileUnsupportedChanges();
  474. }
  475. HotReloadSuggestionsHelper.SetSuggestionActive(HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods);
  476. foreach (var newInlinedMethod in newInlinedMethods) {
  477. inlinedMethodsFound.Add(newInlinedMethod);
  478. }
  479. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Patching, StatEventType.Inlined)).Forget();
  480. }
  481. } catch (Exception e) {
  482. Log.Warning($"Inline method checker ran into an exception. Please contact support with the exception message to investigate the problem. Exception: {e.Message}");
  483. }
  484. }
  485. #endif
  486. static void CheckEditorSettings() {
  487. if (quitting) {
  488. return;
  489. }
  490. CheckAutoRefresh();
  491. CheckScriptCompilation();
  492. CheckPlaymodeTint();
  493. CheckAssetDatabaseRefresh();
  494. }
  495. static void CheckAutoRefresh() {
  496. if (HotReloadPrefs.AllowDisableUnityAutoRefresh && ServerHealthCheck.I.IsServerHealthy) {
  497. AutoRefreshSettingChecker.Apply();
  498. AutoRefreshSettingChecker.Check();
  499. } else {
  500. AutoRefreshSettingChecker.Reset();
  501. }
  502. }
  503. static void CheckScriptCompilation() {
  504. if (HotReloadPrefs.AllowDisableUnityAutoRefresh && ServerHealthCheck.I.IsServerHealthy) {
  505. ScriptCompilationSettingChecker.Apply();
  506. ScriptCompilationSettingChecker.Check();
  507. } else {
  508. ScriptCompilationSettingChecker.Reset();
  509. }
  510. }
  511. static string[] assetExtensionBlacklist = new[] {
  512. ".cs",
  513. // we can add setting to allow scenes to get hot reloaded for users who collaborate (their scenes change externally)
  514. ".unity",
  515. // safer to ignore meta files completely until there's a use-case
  516. ".meta",
  517. // debug files
  518. ".mdb",
  519. ".pdb",
  520. ".compute",
  521. // ".shader", //use assetBlacklist instead
  522. };
  523. public static string[] compileFiles = new[] {
  524. ".asmdef",
  525. ".asmref",
  526. ".rsp",
  527. };
  528. public static string[] plugins = new[] {
  529. // native plugins
  530. ".dll",
  531. ".bundle",
  532. ".dylib",
  533. ".so",
  534. // plugin scripts
  535. ".cpp",
  536. ".h",
  537. ".aar",
  538. ".jar",
  539. ".a",
  540. ".java"
  541. };
  542. static void HandleAssetChange(string assetPath) {
  543. // ignore directories
  544. if (Directory.Exists(assetPath)) {
  545. return;
  546. }
  547. // ignore temp compile files
  548. if (assetPath.Contains("UnityDirMonSyncFile") || assetPath.EndsWith("~", StringComparison.Ordinal)) {
  549. return;
  550. }
  551. foreach (var compileFile in compileFiles) {
  552. if (assetPath.EndsWith(compileFile, StringComparison.Ordinal)) {
  553. HotReloadTimelineHelper.CreateErrorEventEntry($"errors: AssemblyFileEdit: Editing assembly files requires recompiling in Unity. in {assetPath}", entryType: EntryType.Foldout);
  554. _applyingFailed = true;
  555. if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) {
  556. TryRecompileUnsupportedChanges();
  557. }
  558. return;
  559. }
  560. }
  561. // Add plugin changes to unsupported changes list
  562. foreach (var plugin in plugins) {
  563. if (assetPath.EndsWith(plugin, StringComparison.Ordinal)) {
  564. HotReloadTimelineHelper.CreateErrorEventEntry($"errors: NativePluginEdit: Editing native plugins requires recompiling in Unity. in {assetPath}", entryType: EntryType.Foldout);
  565. _applyingFailed = true;
  566. if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) {
  567. TryRecompileUnsupportedChanges();
  568. }
  569. return;
  570. }
  571. }
  572. // ignore file extensions that trigger domain reload
  573. if (!HotReloadPrefs.IncludeShaderChanges) {
  574. if (assetPath.EndsWith(".shader", StringComparison.Ordinal)) {
  575. return;
  576. }
  577. }
  578. foreach (var blacklisted in assetExtensionBlacklist) {
  579. if (assetPath.EndsWith(blacklisted, StringComparison.Ordinal)) {
  580. return;
  581. }
  582. }
  583. if (config?.assetBlacklist != null) {
  584. foreach (var blacklisted in config.assetBlacklist) {
  585. if (assetPath.EndsWith(blacklisted, StringComparison.Ordinal)) {
  586. return;
  587. }
  588. }
  589. }
  590. var path = ToPath(assetPath);
  591. if (path == null) {
  592. return;
  593. }
  594. try {
  595. if (!File.Exists(assetPath)) {
  596. AssetDatabase.DeleteAsset(path);
  597. } else {
  598. AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
  599. }
  600. } catch (Exception e){
  601. Log.Warning($"Refreshing asset at path: {assetPath} failed due to exception: {e}");
  602. }
  603. }
  604. static string ToPath(string assetPath) {
  605. var relativePath = GetRelativePath(assetPath, Path.GetFullPath("Assets"));
  606. var relativePathPackages = GetRelativePath(assetPath, Path.GetFullPath("Packages"));
  607. // ignore files outside assets and packages folders
  608. if (relativePath.StartsWith("..", StringComparison.Ordinal)) {
  609. relativePath = null;
  610. }
  611. if (relativePathPackages.StartsWith("..", StringComparison.Ordinal)) {
  612. relativePathPackages = null;
  613. #if UNITY_2021_1_OR_NEWER
  614. // Might be inside a package "file:"
  615. try {
  616. foreach (var package in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages()) {
  617. if (assetPath.StartsWith(package.resolvedPath.Replace("\\", "/"), StringComparison.Ordinal)) {
  618. relativePathPackages = $"Packages/{package.name}/{assetPath.Substring(package.resolvedPath.Length)}";
  619. break;
  620. }
  621. }
  622. } catch {
  623. // ignore
  624. }
  625. #endif
  626. }
  627. return relativePath ?? relativePathPackages;
  628. }
  629. public static string GetRelativePath(string filespec, string folder) {
  630. Uri pathUri = new Uri(filespec);
  631. Uri folderUri = new Uri(folder);
  632. return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar));
  633. }
  634. static void CheckPlaymodeTint() {
  635. if (config.changePlaymodeTint && ServerHealthCheck.I.IsServerHealthy && Application.isPlaying) {
  636. PlaymodeTintSettingChecker.Apply();
  637. PlaymodeTintSettingChecker.Check();
  638. } else {
  639. PlaymodeTintSettingChecker.Reset();
  640. }
  641. }
  642. static void CheckAssetDatabaseRefresh() {
  643. if (config.disableCompilingFromEditorScripts && ServerHealthCheck.I.IsServerHealthy) {
  644. CompileMethodDetourer.Apply();
  645. } else {
  646. CompileMethodDetourer.Reset();
  647. }
  648. }
  649. static void HandleResponseReceived(MethodPatchResponse response) {
  650. RegisterPatchesResult patchResult = null;
  651. if (response.patches?.Length > 0
  652. || response.alteredFields.Length > 0
  653. || response.removedFieldInitializers.Length > 0
  654. || response.addedFieldInitializerInitializers.Length > 0
  655. || response.addedFieldInitializerFields.Length > 0
  656. ) {
  657. LogBurstHint(response);
  658. patchResult = CodePatcher.I.RegisterPatches(response, persist: true);
  659. CodePatcher.I.SaveAppliedPatches(patchesFilePath).Forget();
  660. }
  661. if (patchResult?.inspectorModified == true) {
  662. // repaint all views calls all gui callbacks but doesn't rebuild the visual tree
  663. // which is needed to hide removed fields
  664. UnityFieldDrawerPatchHelper.repaintVisualTree = true;
  665. InternalEditorUtility.RepaintAllViews();
  666. }
  667. var partiallySupportedChangesFiltered = new List<PartiallySupportedChange>(response.partiallySupportedChanges ?? Array.Empty<PartiallySupportedChange>());
  668. partiallySupportedChangesFiltered.RemoveAll(x => !HotReloadTimelineHelper.GetPartiallySupportedChangePref(x));
  669. if (!HotReloadPrefs.DisplayNewMonobehaviourMethodsAsPartiallySupported && partiallySupportedChangesFiltered.Remove(PartiallySupportedChange.AddMonobehaviourMethod)) {
  670. if (HotReloadSuggestionsHelper.CanShowServerSuggestion(HotReloadSuggestionKind.AddMonobehaviourMethod)) {
  671. HotReloadSuggestionsHelper.SetServerSuggestionShown(HotReloadSuggestionKind.AddMonobehaviourMethod);
  672. }
  673. }
  674. var failuresDeduplicated = new HashSet<string>(response.failures ?? Array.Empty<string>());
  675. foreach (var hotReloadSuggestionKind in response.suggestions) {
  676. if (HotReloadSuggestionsHelper.CanShowServerSuggestion(hotReloadSuggestionKind)) {
  677. HotReloadSuggestionsHelper.SetServerSuggestionShown(hotReloadSuggestionKind);
  678. }
  679. }
  680. var allMethods = patchResult?.patchedSMethods.Select(m => GetExtendedMethodName(m));
  681. if (allMethods == null) {
  682. allMethods = response.removedMethod?.Select(m => GetExtendedMethodName(m)).Distinct(StringComparer.OrdinalIgnoreCase) ?? Array.Empty<string>();
  683. } else {
  684. allMethods = allMethods.Concat(response.removedMethod?.Select(m => GetExtendedMethodName(m)) ?? Array.Empty<string>()).Distinct(StringComparer.OrdinalIgnoreCase);
  685. }
  686. var allFields = (patchResult?.addedFields.Select(f => GetExtendedFieldName(f)) ?? Array.Empty<string>())
  687. .Concat(response.alteredFields?.Select(f => GetExtendedFieldName(f)).Distinct(StringComparer.OrdinalIgnoreCase) ?? Array.Empty<string>())
  688. .Concat(response.patches?.SelectMany(p => p?.propertyAttributesFieldUpdated ?? Array.Empty<SField>()).Select(f => GetExtendedFieldName(f)).Distinct(StringComparer.OrdinalIgnoreCase) ?? Array.Empty<string>())
  689. .Distinct(StringComparer.OrdinalIgnoreCase);
  690. var patchedMembersDisplayNames = allMethods.Concat(allFields).ToArray();
  691. _compileError = response.failures?.Any(failure => failure.Contains("error CS")) ?? false;
  692. _applyingFailed = response.failures?.Length > 0 || patchResult?.patchFailures.Count > 0 || patchResult?.patchExceptions.Count > 0;
  693. _appliedPartially = !_applyingFailed && partiallySupportedChangesFiltered.Count > 0;
  694. _appliedUndetected = patchedMembersDisplayNames.Length == 0;
  695. if (!_compileError) {
  696. lastCompileErrorLog = null;
  697. }
  698. var autoRecompiled = false;
  699. if (_compileError) {
  700. HotReloadTimelineHelper.EventsTimeline.RemoveAll(e => e.alertType == AlertType.CompileError);
  701. foreach (var failure in failuresDeduplicated) {
  702. if (failure.Contains("error CS")) {
  703. HotReloadTimelineHelper.CreateErrorEventEntry(failure);
  704. }
  705. }
  706. if (lastCompileErrorLog != null) {
  707. Log.Error(lastCompileErrorLog);
  708. lastCompileErrorLog = null;
  709. }
  710. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Reload, StatEventType.CompileError), new EditorExtraData {
  711. { StatKey.PatchId, response.id },
  712. }).Forget();
  713. } else if (_applyingFailed) {
  714. if (partiallySupportedChangesFiltered.Count > 0) {
  715. foreach (var responsePartiallySupportedChange in partiallySupportedChangesFiltered) {
  716. HotReloadTimelineHelper.CreatePartiallyAppliedEventEntry(responsePartiallySupportedChange, entryType: EntryType.Child);
  717. }
  718. }
  719. foreach (var failure in failuresDeduplicated) {
  720. HotReloadTimelineHelper.CreateErrorEventEntry(failure, entryType: EntryType.Child);
  721. }
  722. if (patchResult?.patchFailures.Count > 0) {
  723. foreach (var failure in patchResult.patchFailures) {
  724. SMethod method = failure.Item1;
  725. string error = failure.Item2;
  726. HotReloadTimelineHelper.CreatePatchFailureEventEntry(error, methodName: GetMethodName(method), methodSimpleName: method.simpleName, entryType: EntryType.Child);
  727. }
  728. }
  729. if (patchResult?.patchExceptions.Count > 0) {
  730. foreach (var error in patchResult.patchExceptions) {
  731. HotReloadTimelineHelper.CreateErrorEventEntry(error, entryType: EntryType.Child);
  732. }
  733. }
  734. HotReloadTimelineHelper.CreateReloadFinishedWithWarningsEventEntry(patchedMembersDisplayNames: patchedMembersDisplayNames);
  735. HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.UnsupportedChanges);
  736. if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) {
  737. autoRecompiled = TryRecompileUnsupportedChanges();
  738. }
  739. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Reload, StatEventType.Failure), new EditorExtraData {
  740. { StatKey.PatchId, response.id },
  741. }).Forget();
  742. } else if (_appliedPartially) {
  743. foreach (var responsePartiallySupportedChange in partiallySupportedChangesFiltered) {
  744. HotReloadTimelineHelper.CreatePartiallyAppliedEventEntry(responsePartiallySupportedChange, entryType: EntryType.Child, detailed: false);
  745. }
  746. HotReloadTimelineHelper.CreateReloadPartiallyAppliedEventEntry(patchedMethodsDisplayNames: patchedMembersDisplayNames);
  747. if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) {
  748. autoRecompiled = TryRecompileUnsupportedChanges();
  749. }
  750. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Reload, StatEventType.Partial), new EditorExtraData {
  751. { StatKey.PatchId, response.id },
  752. }).Forget();
  753. } else if (_appliedUndetected) {
  754. HotReloadTimelineHelper.CreateReloadUndetectedChangeEventEntry();
  755. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Reload, StatEventType.Undetected), new EditorExtraData {
  756. { StatKey.PatchId, response.id },
  757. }).Forget();
  758. } else {
  759. HotReloadTimelineHelper.CreateReloadFinishedEventEntry(patchedMethodsDisplayNames: patchedMembersDisplayNames);
  760. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Reload, StatEventType.Finished), new EditorExtraData {
  761. { StatKey.PatchId, response.id },
  762. }).Forget();
  763. }
  764. // When patching different assembly, compile error will get removed, even though it's still there
  765. // It's a shortcut we take for simplicity
  766. if (!_compileError) {
  767. HotReloadTimelineHelper.EventsTimeline.RemoveAll(x => x.alertType == AlertType.CompileError);
  768. }
  769. foreach (string responseFailure in response.failures) {
  770. if (responseFailure.Contains("error CS")) {
  771. Log.Error(responseFailure);
  772. } else if (autoRecompiled) {
  773. Log.Info(responseFailure);
  774. } else {
  775. Log.Warning(responseFailure);
  776. }
  777. }
  778. if (patchResult?.patchFailures.Count > 0) {
  779. foreach (var patchResultPatchFailure in patchResult.patchFailures) {
  780. if (autoRecompiled) {
  781. Log.Info(patchResultPatchFailure.Item2);
  782. } else {
  783. Log.Warning(patchResultPatchFailure.Item2);
  784. }
  785. }
  786. }
  787. if (patchResult?.patchExceptions.Count > 0) {
  788. foreach (var patchResultPatchException in patchResult.patchExceptions) {
  789. if (autoRecompiled) {
  790. Log.Info(patchResultPatchException);
  791. } else {
  792. Log.Warning(patchResultPatchException);
  793. }
  794. }
  795. }
  796. // attempt to recompile if previous Unity compilation had compilation errors
  797. // because new changes might've fixed those errors
  798. if (compileChecker.hasCompileErrors) {
  799. HotReloadRunTab.Recompile();
  800. }
  801. if (HotReloadWindow.Current) {
  802. HotReloadWindow.Current.Repaint();
  803. }
  804. HotReloadState.LastPatchId = response.id;
  805. OnPatchHandled?.Invoke((response, patchResult));
  806. }
  807. static string GetExtendedMethodName(SMethod method) {
  808. var colonIndex = method.displayName.IndexOf("::", StringComparison.Ordinal);
  809. if (colonIndex > 0) {
  810. var beforeColon = method.displayName.Substring(0, colonIndex);
  811. var spaceIndex = beforeColon.LastIndexOf(".", StringComparison.Ordinal);
  812. if (spaceIndex > 0) {
  813. var className = beforeColon.Substring(spaceIndex + 1);
  814. return className + "::" + method.simpleName;
  815. }
  816. }
  817. return method.simpleName;
  818. }
  819. static string GetExtendedFieldName(SField field) {
  820. string typeName = field.declaringType.typeName;
  821. var simpleTypeIndex = typeName.LastIndexOf(".", StringComparison.Ordinal);
  822. if (simpleTypeIndex > 0) {
  823. typeName = typeName.Substring(simpleTypeIndex + 1);
  824. }
  825. return $"{typeName}::{field.fieldName}";
  826. }
  827. static string GetMethodName(SMethod method) {
  828. var spaceIndex = method.displayName.IndexOf(" ", StringComparison.Ordinal);
  829. if (spaceIndex > 0) {
  830. return method.displayName.Substring(spaceIndex);
  831. }
  832. return method.displayName;
  833. }
  834. [Conditional("UNITY_2022_2_OR_NEWER")]
  835. static void LogBurstHint(MethodPatchResponse response) {
  836. if(HotReloadPrefs.LoggedBurstHint) {
  837. return;
  838. }
  839. foreach (var patch in response.patches) {
  840. if(patch.unityJobs.Length > 0) {
  841. Debug.LogWarning("A unity job was hot reloaded. " +
  842. "This will cause a harmless warning that can be ignored. " +
  843. $"More info about this can be found here: {Constants.TroubleshootingURL}");
  844. HotReloadPrefs.LoggedBurstHint = true;
  845. break;
  846. }
  847. }
  848. }
  849. private static DateTime? startWaitingForCompile;
  850. static void OnCompilationFinished() {
  851. ServerHealthCheck.instance.CheckHealth();
  852. if(ServerHealthCheck.I.IsServerHealthy) {
  853. startWaitingForCompile = DateTime.UtcNow;
  854. firstPatchAttempted = false;
  855. RequestCompile().Forget();
  856. }
  857. ClearPersistence();
  858. }
  859. static void ClearPersistence() {
  860. Task.Run(() => File.Delete(patchesFilePath));
  861. HotReloadTimelineHelper.ClearPersistance();
  862. }
  863. static bool requestingCompile;
  864. static async Task RequestCompile() {
  865. requestingCompile = true;
  866. try {
  867. await RequestHelper.RequestClearPatches();
  868. await ProjectGeneration.ProjectGeneration.GenerateSlnAndCsprojFiles(Application.dataPath);
  869. await RequestHelper.RequestCompile(scenePath => {
  870. var path = ToPath(scenePath);
  871. if (File.Exists(scenePath) && path != null) {
  872. AssetDatabase.ImportAsset(path, ImportAssetOptions.Default);
  873. }
  874. });
  875. } finally {
  876. requestingCompile = false;
  877. }
  878. }
  879. private static bool stopping;
  880. private static bool starting;
  881. private static DateTime? startupCompletedAt;
  882. private static Tuple<float, string> startupProgress;
  883. internal static bool Started => ServerHealthCheck.I.IsServerHealthy && DownloadProgress == 1 && StartupProgress?.Item1 == 1;
  884. internal static bool Starting => (StartedServerRecently() || ServerHealthCheck.I.IsServerHealthy) && !Started && starting && patchStatus != PatchStatus.CompileError;
  885. internal static bool Stopping => stopping && Running;
  886. internal static bool Compiling => DateTime.UtcNow - startWaitingForCompile < TimeSpan.FromSeconds(5) || patchStatus == PatchStatus.Compiling || HotReloadRunTab.recompiling;
  887. internal static Tuple<float, string> StartupProgress => startupProgress;
  888. /// <summary>
  889. /// We have a button to stop the Hot Reload server.<br/>
  890. /// Store task to ensure only one stop attempt at a time.
  891. /// </summary>
  892. private static DateTime? serverStartedAt;
  893. private static DateTime? serverStoppedAt;
  894. private static DateTime? serverRestartedAt;
  895. private static bool StartedServerRecently() {
  896. return DateTime.UtcNow - serverStartedAt < ServerHealthCheck.HeartBeatTimeout;
  897. }
  898. internal static bool StoppedServerRecently() {
  899. return DateTime.UtcNow - serverStoppedAt < ServerHealthCheck.HeartBeatTimeout || (!StartedServerRecently() && (startupProgress?.Item1 ?? 0) == 0);
  900. }
  901. internal static bool RestartedServerRecently() {
  902. return DateTime.UtcNow - serverRestartedAt < ServerHealthCheck.HeartBeatTimeout;
  903. }
  904. private static bool requestingStart;
  905. private static async Task StartCodePatcher(LoginData loginData = null) {
  906. if (requestingStart || StartedServerRecently()) {
  907. return;
  908. }
  909. stopping = false;
  910. starting = true;
  911. var exposeToNetwork = HotReloadPrefs.ExposeServerToLocalNetwork;
  912. var allAssetChanges = HotReloadPrefs.AllAssetChanges;
  913. var disableConsoleWindow = HotReloadPrefs.DisableConsoleWindow;
  914. var isReleaseMode = RequestHelper.IsReleaseMode();
  915. var detailedErrorReporting = !HotReloadPrefs.DisableDetailedErrorReporting;
  916. CodePatcher.I.ClearPatchedMethods();
  917. RecordActiveDaysForRateApp();
  918. try {
  919. requestingStart = true;
  920. startupProgress = Tuple.Create(0f, "Starting Hot Reload");
  921. serverStartedAt = DateTime.UtcNow;
  922. await HotReloadCli.StartAsync(exposeToNetwork, allAssetChanges, disableConsoleWindow, isReleaseMode, detailedErrorReporting, loginData).ConfigureAwait(false);
  923. }
  924. catch (Exception ex) {
  925. ThreadUtility.LogException(ex);
  926. }
  927. finally {
  928. requestingStart = false;
  929. }
  930. }
  931. private static bool requestingStop;
  932. internal static async Task StopCodePatcher(bool recompileOnDone = false) {
  933. stopping = true;
  934. starting = false;
  935. if (requestingStop) {
  936. if (recompileOnDone) {
  937. await ThreadUtility.SwitchToMainThread();
  938. HotReloadRunTab.Recompile();
  939. }
  940. return;
  941. }
  942. CodePatcher.I.ClearPatchedMethods();
  943. HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
  944. try {
  945. requestingStop = true;
  946. await HotReloadCli.StopAsync().ConfigureAwait(false);
  947. serverStoppedAt = DateTime.UtcNow;
  948. await ThreadUtility.SwitchToMainThread();
  949. if (recompileOnDone) {
  950. HotReloadRunTab.Recompile();
  951. }
  952. startupProgress = null;
  953. }
  954. catch (Exception ex) {
  955. ThreadUtility.LogException(ex);
  956. }
  957. finally {
  958. requestingStop = false;
  959. }
  960. }
  961. private static bool requestingRestart;
  962. internal static async Task RestartCodePatcher() {
  963. if (requestingRestart) {
  964. return;
  965. }
  966. try {
  967. requestingRestart = true;
  968. await StopCodePatcher();
  969. await DownloadAndRun();
  970. serverRestartedAt = DateTime.UtcNow;
  971. }
  972. finally {
  973. requestingRestart = false;
  974. }
  975. }
  976. private static bool requestingDownloadAndRun;
  977. internal static float DownloadProgress => serverDownloader.Progress;
  978. internal static bool DownloadRequired => DownloadProgress < 1f;
  979. internal static bool DownloadStarted => serverDownloader.Started;
  980. internal static bool RequestingDownloadAndRun => requestingDownloadAndRun;
  981. internal static async Task<bool> DownloadAndRun(LoginData loginData = null, bool recompileOnDone = false) {
  982. if (requestingDownloadAndRun) {
  983. return false;
  984. }
  985. stopping = false;
  986. requestingDownloadAndRun = true;
  987. try {
  988. if (DownloadRequired) {
  989. var ok = await serverDownloader.PromptForDownload();
  990. if (!ok) {
  991. return false;
  992. }
  993. }
  994. await StartCodePatcher(loginData);
  995. await ThreadUtility.SwitchToMainThread();
  996. if (HotReloadPrefs.DeactivateHotReload) {
  997. HotReloadPrefs.DeactivateHotReload = false;
  998. HotReloadRunTab.Recompile();
  999. }
  1000. return true;
  1001. } finally {
  1002. requestingDownloadAndRun = false;
  1003. }
  1004. }
  1005. private const int SERVER_POLL_FREQUENCY_ON_STARTUP_MS = 500;
  1006. private const int SERVER_POLL_FREQUENCY_AFTER_STARTUP_MS = 2000;
  1007. private static int GetPollFrequency() {
  1008. return (startupProgress != null && startupProgress.Item1 < 1) || StartedServerRecently()
  1009. ? SERVER_POLL_FREQUENCY_ON_STARTUP_MS
  1010. : SERVER_POLL_FREQUENCY_AFTER_STARTUP_MS;
  1011. }
  1012. internal static bool RequestingLoginInfo { get; set; }
  1013. [CanBeNull] internal static LoginStatusResponse Status { get; private set; }
  1014. internal static void HandleStatus(LoginStatusResponse resp) {
  1015. if (resp == null) {
  1016. return;
  1017. }
  1018. Attribution.RegisterLogin(resp);
  1019. bool consumptionsChanged = Status?.freeSessionRunning != resp.freeSessionRunning || Status?.freeSessionEndTime != resp.freeSessionEndTime;
  1020. bool expiresAtChanged = Status?.licenseExpiresAt != resp.licenseExpiresAt;
  1021. if (!EditorCodePatcher.LoginNotRequired
  1022. && resp.consumptionsUnavailableReason == ConsumptionsUnavailableReason.UnrecoverableError
  1023. && Status?.consumptionsUnavailableReason != ConsumptionsUnavailableReason.UnrecoverableError
  1024. ) {
  1025. Log.Error("Free charges unavailabe. Please contact support if the issue persists.");
  1026. }
  1027. if (!RequestingLoginInfo && resp.requestError == null) {
  1028. Status = resp;
  1029. }
  1030. if (resp.lastLicenseError == null) {
  1031. // If we got success, we should always show an error next time it comes up
  1032. HotReloadPrefs.ErrorHidden = false;
  1033. }
  1034. var oldStartupProgress = startupProgress;
  1035. var newStartupProgress = Tuple.Create(
  1036. resp.startupProgress,
  1037. string.IsNullOrEmpty(resp.startupStatus) ? "Starting Hot Reload" : resp.startupStatus);
  1038. startupProgress = newStartupProgress;
  1039. // ReSharper disable once CompareOfFloatsByEqualityOperator
  1040. if (startupCompletedAt == null && newStartupProgress.Item1 == 1f) {
  1041. startupCompletedAt = DateTime.UtcNow;
  1042. }
  1043. if (oldStartupProgress == null
  1044. || Math.Abs(oldStartupProgress.Item1 - newStartupProgress.Item1) > 0
  1045. || oldStartupProgress.Item2 != newStartupProgress.Item2
  1046. || consumptionsChanged
  1047. || expiresAtChanged
  1048. ) {
  1049. // Send project files state now that server can receive requests (only needed for player builds)
  1050. TryPrepareBuildInfo();
  1051. }
  1052. }
  1053. internal static async Task RequestLogin(string email, string password) {
  1054. RequestingLoginInfo = true;
  1055. try {
  1056. int i = 0;
  1057. while (!Running && i < 100) {
  1058. await Task.Delay(100);
  1059. i++;
  1060. }
  1061. Status = await RequestHelper.RequestLogin(email, password, 10);
  1062. // set to false so new error is shown
  1063. HotReloadPrefs.ErrorHidden = false;
  1064. if (Status?.isLicensed == true) {
  1065. HotReloadPrefs.LicenseEmail = email;
  1066. HotReloadPrefs.LicensePassword = Status.initialPassword ?? password;
  1067. }
  1068. } finally {
  1069. RequestingLoginInfo = false;
  1070. }
  1071. }
  1072. private static bool requestingServerInfo;
  1073. private static long lastServerPoll;
  1074. private static bool running;
  1075. internal static bool Running => ServerHealthCheck.I.IsServerHealthy;
  1076. internal static void RequestServerInfo() {
  1077. if (requestingServerInfo) {
  1078. return;
  1079. }
  1080. RequestServerInfoAsync().Forget();
  1081. }
  1082. private static async Task RequestServerInfoAsync() {
  1083. requestingServerInfo = true;
  1084. try {
  1085. await RequestServerInfoCore();
  1086. } finally {
  1087. requestingServerInfo = false;
  1088. }
  1089. }
  1090. private static async Task RequestServerInfoCore() {
  1091. var pollFrequency = GetPollFrequency();
  1092. // Delay until we've hit the poll request frequency
  1093. var waitMs = (int)Mathf.Clamp(pollFrequency - ((DateTime.Now.Ticks / (float)TimeSpan.TicksPerMillisecond) - lastServerPoll), 0, pollFrequency);
  1094. await Task.Delay(waitMs);
  1095. if (!ServerHealthCheck.I.IsServerHealthy) {
  1096. return;
  1097. }
  1098. var resp = await RequestHelper.GetLoginStatus(30);
  1099. HandleStatus(resp);
  1100. lastServerPoll = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
  1101. }
  1102. }
  1103. // IMPORTANT: don't change the names of the methods
  1104. internal static class UnityFieldDrawerPatchHelper {
  1105. internal static void PatchCustom(Rect contentRect, UnityEditor.Editor __instance) {
  1106. if (__instance.target) {
  1107. FieldDrawerUtil.DrawFromObject(__instance.target);
  1108. }
  1109. }
  1110. internal static void PatchDefault(UnityEditor.Editor __instance) {
  1111. if (__instance.target) {
  1112. FieldDrawerUtil.DrawFromObject(__instance.target);
  1113. }
  1114. }
  1115. internal static bool repaintVisualTree;
  1116. internal static void PatchFillDefaultInspector(VisualElement container, SerializedObject serializedObject, UnityEditor.Editor editor) {
  1117. HideChildren(container, serializedObject);
  1118. if (editor.target) {
  1119. var child = new IMGUIContainer((() =>
  1120. {
  1121. FieldDrawerUtil.DrawFromObject(editor.target);
  1122. if (repaintVisualTree) {
  1123. HideChildren(container, serializedObject);
  1124. ResetInvalidatedInspectorFields(container, serializedObject);
  1125. // Mark dirty to repaint the visual tree
  1126. container.MarkDirtyRepaint();
  1127. repaintVisualTree = false;
  1128. }
  1129. }));
  1130. child.name = "SingularityGroup.HotReload.FieldDrawer";
  1131. container.Add(child);
  1132. }
  1133. }
  1134. static List<VisualElement> childrenToRemove = new List<VisualElement>();
  1135. static void HideChildren(VisualElement container, SerializedObject serializedObject) {
  1136. if (container == null) {
  1137. return;
  1138. }
  1139. childrenToRemove.Clear();
  1140. foreach (var child in container.Children()) {
  1141. if (!(child is PropertyField propertyField)) {
  1142. continue;
  1143. }
  1144. try {
  1145. if (serializedObject != null && serializedObject.targetObject && UnityFieldHelper.IsFieldHidden(serializedObject.targetObject.GetType(), serializedObject.FindProperty(propertyField.bindingPath)?.name ?? "")) {
  1146. childrenToRemove.Add(child);
  1147. }
  1148. } catch (NullReferenceException) {
  1149. // serializedObject.targetObject throws nullref in cases where e.g. exising playmode
  1150. }
  1151. }
  1152. foreach (var child in childrenToRemove) {
  1153. container.Remove(child);
  1154. }
  1155. childrenToRemove.Clear();
  1156. }
  1157. static void ResetInvalidatedInspectorFields(VisualElement container, SerializedObject serializedObject) {
  1158. if (container == null || serializedObject == null) {
  1159. return;
  1160. }
  1161. foreach (var child in container.Children()) {
  1162. if (!(child is PropertyField propertyField)) {
  1163. continue;
  1164. }
  1165. try {
  1166. var prop = serializedObject.FindProperty(propertyField.bindingPath);
  1167. if (prop != null && serializedObject.targetObject && UnityFieldHelper.HasFieldInspectorCacheInvalidation(serializedObject.targetObject.GetType(), prop.name ?? "")) {
  1168. child.GetType().GetMethod("Reset", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(SerializedProperty) }, null)?.Invoke(child, new object[] { prop });
  1169. }
  1170. } catch (NullReferenceException) {
  1171. // serializedObject.targetObject throws nullref in cases where e.g. exising playmode
  1172. }
  1173. }
  1174. }
  1175. internal static bool GetHandlerPrefix(
  1176. SerializedProperty property,
  1177. ref object __result
  1178. ) {
  1179. if (property == null || property.serializedObject == null || !property.serializedObject.targetObject) {
  1180. // do nothing
  1181. return true;
  1182. }
  1183. if (UnityFieldHelper.TryInvalidateFieldInspectorCache(property.serializedObject.targetObject.GetType(), property.name)) {
  1184. __result = null;
  1185. return false;
  1186. }
  1187. return true;
  1188. }
  1189. internal static bool GetFieldAttributesPrefix(
  1190. FieldInfo field,
  1191. ref List<PropertyAttribute> __result
  1192. ) {
  1193. if (field == null) {
  1194. // do nothing
  1195. return true;
  1196. }
  1197. List<PropertyAttribute> result;
  1198. if (UnityFieldHelper.TryGetInspectorFieldAttributes(field, out result)) {
  1199. __result = result;
  1200. return false;
  1201. }
  1202. return true;
  1203. }
  1204. internal static bool PropertyFieldPrefix(
  1205. Rect position,
  1206. UnityEditor.SerializedProperty property,
  1207. GUIContent label,
  1208. bool includeChildren,
  1209. Rect visibleArea,
  1210. ref bool __result
  1211. ) {
  1212. if (property == null || property.serializedObject == null || !property.serializedObject.targetObject) {
  1213. // do nothing
  1214. return true;
  1215. }
  1216. if (UnityFieldHelper.IsFieldHidden(property.serializedObject.targetObject.GetType(), property.name)) {
  1217. // make sure field doesn't take any space
  1218. __result = false;
  1219. return false; // Skip original method
  1220. }
  1221. return true; // Continue with original method
  1222. }
  1223. internal static bool GetHightPrefix(
  1224. UnityEditor.SerializedProperty property, GUIContent label, bool includeChildren,
  1225. ref float __result
  1226. ) {
  1227. if (property == null || property.serializedObject == null || !property.serializedObject.targetObject) {
  1228. // do nothing
  1229. return true;
  1230. }
  1231. if (UnityFieldHelper.IsFieldHidden(property.serializedObject.targetObject.GetType(), property.name)) {
  1232. // make sure field doesn't take any space
  1233. __result = 0.0f;
  1234. return false; // Skip original method
  1235. }
  1236. return true; // Continue with original method
  1237. }
  1238. }
  1239. }