1419 lines
66 KiB
C#
1419 lines
66 KiB
C#
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 UnityEditor;
|
|
using UnityEngine;
|
|
using Debug = UnityEngine.Debug;
|
|
using Task = System.Threading.Tasks.Task;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using SingularityGroup.HotReload.Editor.Localization;
|
|
using SingularityGroup.HotReload.Newtonsoft.Json;
|
|
using UnityEditor.Build;
|
|
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;
|
|
}
|
|
Translations.LoadDefaultLocalization();
|
|
SingularityGroup.HotReload.Localization.Translations.LoadDefaultLocalization();
|
|
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), typeof(VisualElement));
|
|
|
|
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.assemblyCompilationFinished += (string _, CompilerMessage[] messages) => {
|
|
foreach (var message in messages) {
|
|
if (message.type != CompilerMessageType.Error) {
|
|
continue;
|
|
}
|
|
if (!message.message.Contains("Sirenix")) {
|
|
continue;
|
|
}
|
|
if (message.message.Contains("CS0012")
|
|
|| message.message.Contains("CS0234")
|
|
|| message.message.Contains("CS0246")
|
|
|| message.message.Contains("CS9286")
|
|
) {
|
|
#if UNITY_2021_1_OR_NEWER
|
|
var target = NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
|
|
var symbols = PlayerSettings.GetScriptingDefineSymbols(target).Split(";").ToList();
|
|
symbols.Remove("ODIN_INSPECTOR");
|
|
PlayerSettings.SetScriptingDefineSymbols(target, string.Join(";", symbols));
|
|
#else
|
|
var symbols = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup).Split(';').ToList();
|
|
symbols.Remove("ODIN_INSPECTOR");
|
|
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, string.Join(";", symbols));
|
|
#endif
|
|
}
|
|
}
|
|
};
|
|
|
|
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
|
|
|| !isPlaying && !HotReloadPrefs.AutoRecompileUnsupportedChangesInEditMode
|
|
) {
|
|
return false;
|
|
}
|
|
RecompileUnsupportedChanges();
|
|
return true;
|
|
}
|
|
|
|
public static void RecompileUnsupportedChanges() {
|
|
if (HotReloadPrefs.ShowCompilingUnsupportedNotifications) {
|
|
EditorWindowHelper.ShowNotification(EditorWindowHelper.NotificationStatus.NeedsRecompile);
|
|
}
|
|
if (EditorApplication.isPlaying) {
|
|
HotReloadState.RecompiledUnsupportedChangesInPlaymode = true;
|
|
}
|
|
HotReloadRunTab.Recompile();
|
|
}
|
|
|
|
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(Translations.Dialogs.DialogTitleRestartServer,
|
|
Translations.Dialogs.DialogMessageRestartUpdate,
|
|
Translations.Dialogs.DialogButtonRestartServer, Translations.Dialogs.DialogButtonDontRestart);
|
|
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;
|
|
}
|
|
if (!Application.isPlaying && HotReloadPrefs.PauseHotReloadInEditMode) {
|
|
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 (!ServerHealthCheck.I.IsServerHealthy) {
|
|
stopping = false;
|
|
}
|
|
if (startupProgress?.Item1 == 1) {
|
|
starting = false;
|
|
}
|
|
if (!_requestingFlushErrors && Running) {
|
|
RequestFlushErrors().Forget();
|
|
}
|
|
|
|
if (!Application.isPlaying && HotReloadPrefs.PauseHotReloadInEditMode) {
|
|
return;
|
|
}
|
|
|
|
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(Translations.Errors.InfoDebuggerAttached);
|
|
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
|
|
}
|
|
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(Translations.Errors.WarningInlinedMethods);
|
|
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(Translations.Errors.WarningInlineMethodChecker, 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)
|
|
|| assetPath.Contains("StreamingAssets")
|
|
) {
|
|
return;
|
|
}
|
|
foreach (var compileFile in compileFiles) {
|
|
if (assetPath.EndsWith(compileFile, StringComparison.Ordinal)) {
|
|
HotReloadTimelineHelper.CreateErrorEventEntry(string.Format(Translations.Utility.AssemblyFileEditError, 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(string.Format(Translations.Utility.NativePluginEditError, 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(Translations.Errors.WarningRefreshingAssetFailed, assetPath, 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) {
|
|
if (!disableServerLogs) {
|
|
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();
|
|
}
|
|
|
|
if (!autoRecompiled && patchResult?.inspectorFieldAdded == true && HotReloadPrefs.AutoRecompileInspectorFieldsEdit && !Application.isPlaying) {
|
|
HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.UnsupportedChanges);
|
|
RecompileUnsupportedChanges();
|
|
autoRecompiled = true;
|
|
HotReloadTimelineHelper.CreateErrorEventEntry(Translations.Utility.InspectorFieldChangeError, entryType: EntryType.Child);
|
|
HotReloadTimelineHelper.CreateReloadFinishedWithWarningsEventEntry();
|
|
Log.Info(Translations.Errors.InfoInspectorFieldRecompile);
|
|
}
|
|
|
|
// 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") && !disableServerLogs) {
|
|
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(string.Format(Translations.Errors.WarningUnityJobHotReloaded, 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, Translations.UI.StartingHotReloadMessage);
|
|
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.isLicensed
|
|
&& resp.consumptionsUnavailableReason == ConsumptionsUnavailableReason.UnrecoverableError
|
|
&& Status?.consumptionsUnavailableReason != ConsumptionsUnavailableReason.UnrecoverableError
|
|
) {
|
|
Log.Error(Translations.Errors.ErrorFreeChargesUnavailable);
|
|
}
|
|
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) ? Translations.UI.StartingHotReloadMessage : 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
|
|
}
|
|
}
|
|
}
|