using System; using System.Collections.Generic; using System.IO; using SingularityGroup.HotReload.DTO; using SingularityGroup.HotReload.EditorDependencies; using UnityEditor; using UnityEditor.Compilation; using UnityEngine; using Color = UnityEngine.Color; using Task = System.Threading.Tasks.Task; #if UNITY_2019_4_OR_NEWER using Unity.CodeEditor; #endif namespace SingularityGroup.HotReload.Editor { internal class ErrorData { public string fileName; public string error; public TextAsset file; public int lineNumber; public string stacktrace; public string linkString; private static string[] supportedPaths = new[] { Path.GetFullPath("Assets"), Path.GetFullPath("Plugins") }; public static ErrorData GetErrorData(string errorString) { // Get the relevant file name string stackTrace = errorString; string fileName = null; try { int csIndex = 0; int attempt = 0; do { csIndex = errorString.IndexOf(".cs", csIndex + 1, StringComparison.Ordinal); if (csIndex == -1) { break; } int fileNameStartIndex = csIndex - 1; for (; fileNameStartIndex >= 0; fileNameStartIndex--) { if (!char.IsLetter(errorString[fileNameStartIndex])) { if (errorString.Contains("error CS")) { fileName = errorString.Substring(fileNameStartIndex + 1, csIndex - fileNameStartIndex + ".cs".Length - 1); } else { fileName = errorString.Substring(fileNameStartIndex, csIndex - fileNameStartIndex + ".cs".Length); } break; } } } while (attempt++ < 100 && fileName == null); } catch { // ignore } fileName = fileName ?? "Tap to show stacktrace"; // Get the error string error = (errorString.Contains("error CS") ? "Compile error, " : "Unsupported change detected, ") + "tap here to see more."; int endOfError = errorString.IndexOf(". in ", StringComparison.Ordinal); string specialChars = "\"'/\\"; char[] characters = specialChars.ToCharArray(); int specialChar = errorString.IndexOfAny(characters); try { if (errorString.Contains("error CS") ) { error = errorString.Substring(errorString.IndexOf("error CS", StringComparison.Ordinal), errorString.Length - errorString.IndexOf("error CS", StringComparison.Ordinal)).Trim(); using (StringReader reader = new StringReader(error)) { string line; while ((line = reader.ReadLine()) != null) { error = line; break; } } } else if (errorString.StartsWith("errors:", StringComparison.Ordinal) && endOfError > 0) { error = errorString.Substring("errors: ".Length, endOfError - "errors: ".Length).Trim(); } else if (errorString.StartsWith("errors:", StringComparison.Ordinal) && specialChar > 0) { error = errorString.Substring("errors: ".Length, specialChar - "errors: ".Length).Trim(); } } catch { // ignore } // Get relative path TextAsset file = null; try { foreach (var path in supportedPaths) { int lastprojectIndex = 0; int attempt = 0; while (attempt++ < 100 && !file) { lastprojectIndex = errorString.IndexOf(path, lastprojectIndex + 1, StringComparison.Ordinal); if (lastprojectIndex == -1) { break; } var fullCsIndex = errorString.IndexOf(".cs", lastprojectIndex, StringComparison.Ordinal); var l = fullCsIndex - lastprojectIndex + ".cs".Length; if (l <= 0) { continue; } var candidateAbsolutePath = errorString.Substring(lastprojectIndex, fullCsIndex - lastprojectIndex + ".cs".Length); var candidateRelativePath = EditorCodePatcher.GetRelativePath(filespec: candidateAbsolutePath, folder: path); file = AssetDatabase.LoadAssetAtPath(candidateRelativePath); } } } catch { // ignore } // Get the line number int lineNumber = 0; try { int lastIndex = 0; int attempt = 0; do { lastIndex = errorString.IndexOf(fileName, lastIndex + 1, StringComparison.Ordinal); if (lastIndex == -1) { break; } var part = errorString.Substring(lastIndex + fileName.Length); if (!part.StartsWith(errorString.Contains("error CS") ? "(" : ":", StringComparison.Ordinal) || part.Length == 1 || !char.IsDigit(part[1]) ) { continue; } int y = 1; for (; y < part.Length; y++) { if (!char.IsDigit(part[y])) { break; } } if (int.TryParse(part.Substring(1, errorString.Contains("error CS") ? y - 1 : y), out lineNumber)) { break; } } while (attempt++ < 100); } catch { //ignore } return new ErrorData() { fileName = fileName, error = error, file = file, lineNumber = lineNumber, stacktrace = stackTrace, linkString = lineNumber > 0 ? fileName + ":" + lineNumber : fileName }; } } internal struct HotReloadRunTabState { public readonly bool spinnerActive; public readonly string indicationIconPath; public readonly bool requestingDownloadAndRun; public readonly bool starting; public readonly bool stopping; public readonly bool running; public readonly Tuple startupProgress; public readonly string indicationStatusText; public readonly LoginStatusResponse loginStatus; public readonly bool downloadRequired; public readonly bool downloadStarted; public readonly bool requestingLoginInfo; public readonly RedeemStage redeemStage; public readonly int suggestionCount; public HotReloadRunTabState( bool spinnerActive, string indicationIconPath, bool requestingDownloadAndRun, bool starting, bool stopping, bool running, Tuple startupProgress, string indicationStatusText, LoginStatusResponse loginStatus, bool downloadRequired, bool downloadStarted, bool requestingLoginInfo, RedeemStage redeemStage, int suggestionCount ) { this.spinnerActive = spinnerActive; this.indicationIconPath = indicationIconPath; this.requestingDownloadAndRun = requestingDownloadAndRun; this.starting = starting; this.stopping = stopping; this.running = running; this.startupProgress = startupProgress; this.indicationStatusText = indicationStatusText; this.loginStatus = loginStatus; this.downloadRequired = downloadRequired; this.downloadStarted = downloadStarted; this.requestingLoginInfo = requestingLoginInfo; this.redeemStage = redeemStage; this.suggestionCount = suggestionCount; } public static HotReloadRunTabState Current => new HotReloadRunTabState( spinnerActive: EditorIndicationState.SpinnerActive, indicationIconPath: EditorIndicationState.IndicationIconPath, requestingDownloadAndRun: EditorCodePatcher.RequestingDownloadAndRun, starting: EditorCodePatcher.Starting, stopping: EditorCodePatcher.Stopping, running: EditorCodePatcher.Running, startupProgress: EditorCodePatcher.StartupProgress, indicationStatusText: EditorIndicationState.IndicationStatusText, loginStatus: EditorCodePatcher.Status, downloadRequired: EditorCodePatcher.DownloadRequired, downloadStarted: EditorCodePatcher.DownloadStarted, requestingLoginInfo: EditorCodePatcher.RequestingLoginInfo, redeemStage: RedeemLicenseHelper.I.RedeemStage, suggestionCount: HotReloadTimelineHelper.Suggestions.Count ); } internal struct LicenseErrorData { public readonly string description; public bool showBuyButton; public string buyButtonText; public readonly bool showLoginButton; public readonly string loginButtonText; public readonly bool showSupportButton; public readonly string supportButtonText; public readonly bool showManageLicenseButton; public readonly string manageLicenseButtonText; public LicenseErrorData(string description, bool showManageLicenseButton = false, string manageLicenseButtonText = "", string loginButtonText = "", bool showSupportButton = false, string supportButtonText = "", bool showBuyButton = false, string buyButtonText = "", bool showLoginButton = false) { this.description = description; this.showManageLicenseButton = showManageLicenseButton; this.manageLicenseButtonText = manageLicenseButtonText; this.loginButtonText = loginButtonText; this.showSupportButton = showSupportButton; this.supportButtonText = supportButtonText; this.showBuyButton = showBuyButton; this.buyButtonText = buyButtonText; this.showLoginButton = showLoginButton; } } internal class HotReloadRunTab : HotReloadTabBase { private static string _pendingEmail; private static string _pendingPassword; private string _pendingPromoCode; private bool _requestingActivatePromoCode; private static Tuple _activateInfoMessage; private HotReloadRunTabState currentState => _window.RunTabState; // Has Indie or Pro license (even if not currenctly active) public bool HasPayedLicense => currentState.loginStatus != null && (currentState.loginStatus.isIndieLicense || currentState.loginStatus.isBusinessLicense); public bool TrialLicense => currentState.loginStatus != null && (currentState.loginStatus?.isTrial == true); private Vector2 _patchedMethodsScrollPos; private Vector2 _runTabScrollPos; private string promoCodeError; private MessageType promoCodeErrorType; private bool promoCodeActivatedThisSession; public HotReloadRunTab(HotReloadWindow window) : base(window, "Run", "forward", "Run and monitor the current Hot Reload session.") { } public override void OnGUI() { using(new EditorGUILayout.VerticalScope()) { OnGUICore(); } } internal static bool ShouldRenderConsumption(HotReloadRunTabState currentState) => (currentState.running && !currentState.starting && !currentState.stopping && currentState.loginStatus?.isLicensed != true && currentState.loginStatus?.isFree != true && !EditorCodePatcher.LoginNotRequired) && !(currentState.loginStatus == null || currentState.loginStatus.isFree); void OnGUICore() { using (var scope = new EditorGUILayout.ScrollViewScope(_runTabScrollPos, GUI.skin.horizontalScrollbar, GUI.skin.verticalScrollbar, GUILayout.MaxHeight(Math.Max(HotReloadWindowStyles.windowScreenHeight, 800)), GUILayout.MaxWidth(Math.Max(HotReloadWindowStyles.windowScreenWidth, 800)))) { _runTabScrollPos.x = scope.scrollPosition.x; _runTabScrollPos.y = scope.scrollPosition.y; using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.DynamiSection)) { if (HotReloadWindowStyles.windowScreenWidth > Constants.UpgradeLicenseNoteHideWidth && HotReloadWindowStyles.windowScreenHeight > Constants.UpgradeLicenseNoteHideHeight ) { RenderUpgradeLicenseNote(currentState, HotReloadWindowStyles.UpgradeLicenseButtonStyle); } RenderIndicationPanel(); if (CanRenderBars(currentState)) { RenderBars(currentState); // clear red dot next time button shows HotReloadState.ShowingRedDot = false; } } } // At the end to not fuck up rendering https://answers.unity.com/questions/400454/argumentexception-getting-control-0s-position-in-a-1.html var renderStart = !EditorCodePatcher.Running && !EditorCodePatcher.Starting && !currentState.requestingDownloadAndRun && currentState.redeemStage == RedeemStage.None; var e = Event.current; if (renderStart && e.type == EventType.KeyUp && (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) ) { EditorCodePatcher.DownloadAndRun().Forget(); } } internal static void RenderUpgradeLicenseNote(HotReloadRunTabState currentState, GUIStyle style) { var isIndie = RedeemLicenseHelper.I.RegistrationOutcome == RegistrationOutcome.Indie || EditorCodePatcher.licenseType == UnityLicenseType.UnityPersonalPlus; if (RedeemLicenseHelper.I.RegistrationOutcome == RegistrationOutcome.Business && currentState.loginStatus?.isBusinessLicense != true && EditorCodePatcher.Running && (PackageConst.IsAssetStoreBuild || HotReloadPrefs.RateAppShown) ) { // Warn asset store users they need to buy a business license // Website users get reminded after using Hot Reload for 5+ days RenderBusinessLicenseInfo(style); } else if (isIndie && HotReloadPrefs.RateAppShown && !PackageConst.IsAssetStoreBuild && EditorCodePatcher.Running && currentState.loginStatus?.isBusinessLicense != true && currentState.loginStatus?.isIndieLicense != true ) { // Reminder users they need to buy an indie license RenderIndieLicenseInfo(style); } } internal static bool CanRenderBars(HotReloadRunTabState currentState) { return HotReloadWindowStyles.windowScreenHeight > Constants.EventsListHideHeight && HotReloadWindowStyles.windowScreenWidth > Constants.EventsListHideWidth && !currentState.starting && !currentState.stopping && !currentState.requestingDownloadAndRun ; } static Texture2D GetFoldoutIcon(AlertEntry alertEntry) { InvertibleIcon alertIcon = InvertibleIcon.FoldoutClosed; if (HotReloadTimelineHelper.expandedEntries.Contains(alertEntry)) { alertIcon = InvertibleIcon.FoldoutOpen; } return GUIHelper.GetInvertibleIcon(alertIcon); } static void ToggleEntry(AlertEntry alertEntry) { if (HotReloadTimelineHelper.expandedEntries.Contains(alertEntry)) { HotReloadTimelineHelper.expandedEntries.Remove(alertEntry); } else { HotReloadTimelineHelper.expandedEntries.Add(alertEntry); } } static void RenderEntries(TimelineType timelineType) { List alertEntries; alertEntries = timelineType == TimelineType.Suggestions ? HotReloadTimelineHelper.Suggestions : HotReloadTimelineHelper.EventsTimeline; bool skipChildren = false; for (int i = 0; i < alertEntries.Count; i++) { var alertEntry = alertEntries[i]; if (i > HotReloadTimelineHelper.maxVisibleEntries && alertEntry.entryType != EntryType.Child) { break; } if (timelineType != TimelineType.Suggestions) { if (alertEntry.entryType != EntryType.Child && !enabledFilters.Contains(alertEntry.alertType) ) { skipChildren = true; continue; } else if (alertEntry.entryType == EntryType.Child && skipChildren) { continue; } else { skipChildren = false; } } EntryType entryType = alertEntry.entryType; string title = $" {alertEntry.title}{(!string.IsNullOrEmpty(alertEntry.shortDescription) ? $": {alertEntry.shortDescription}": "")}"; Texture2D icon = null; GUIStyle style; if (entryType != EntryType.Child) { icon = GUIHelper.GetLocalIcon(HotReloadTimelineHelper.alertIconString[alertEntry.iconType]); } if (entryType == EntryType.Child) { style = HotReloadWindowStyles.ChildBarStyle; } else if (entryType == EntryType.Foldout) { style = HotReloadWindowStyles.FoldoutBarStyle; } else { style = HotReloadWindowStyles.BarStyle; } Rect startRect; using (new EditorGUILayout.HorizontalScope()) { GUILayout.Space(0); Rect spaceRect = GUILayoutUtility.GetLastRect(); // entry header foldout arrow if (entryType == EntryType.Foldout) { GUI.Label(new Rect(spaceRect.x + 3, spaceRect.y, 20, 20), new GUIContent(GetFoldoutIcon(alertEntry))); } else if (entryType == EntryType.Child) { GUI.Label(new Rect(spaceRect.x + 26, spaceRect.y + 2, 20, 20), new GUIContent(GetFoldoutIcon(alertEntry))); } // a workaround to limit the width of the label GUILayout.Label(new GUIContent(""), style); startRect = GUILayoutUtility.GetLastRect(); GUI.Label(startRect, new GUIContent(title, icon), style); } bool clickableDescription = (alertEntry.title == "Unsupported change" || alertEntry.title == "Compile error" || alertEntry.title == "Failed applying patch to method") && alertEntry.alertData.alertEntryType != AlertEntryType.InlinedMethod; if (HotReloadTimelineHelper.expandedEntries.Contains(alertEntry) || alertEntry.alertType == AlertType.CompileError) { using (new EditorGUILayout.VerticalScope()) { using (new EditorGUILayout.HorizontalScope()) { using (new EditorGUILayout.VerticalScope(entryType == EntryType.Child ? HotReloadWindowStyles.ChildEntryBoxStyle : HotReloadWindowStyles.EntryBoxStyle)) { if (alertEntry.alertType == AlertType.Suggestion || !clickableDescription) { GUILayout.Label(alertEntry.description, HotReloadWindowStyles.LabelStyle); } if (alertEntry.actionData != null) { alertEntry.actionData.Invoke(); } GUILayout.Space(5f); } } } } // remove button if (timelineType == TimelineType.Suggestions && alertEntry.hasExitButton) { var isClick = GUI.Button(new Rect(startRect.x + startRect.width - 20, startRect.y + 2, 20, 20), new GUIContent(GUIHelper.GetInvertibleIcon(InvertibleIcon.Close)), HotReloadWindowStyles.RemoveIconStyle); if (isClick) { HotReloadTimelineHelper.EventsTimeline.Remove(alertEntry); var kind = HotReloadSuggestionsHelper.FindSuggestionKind(alertEntry); if (kind != null) { HotReloadSuggestionsHelper.SetSuggestionInactive((HotReloadSuggestionKind)kind); } _instantRepaint = true; } } // Extend background to whole entry var endRect = GUILayoutUtility.GetLastRect(); if (GUI.Button(new Rect(startRect) { height = endRect.y - startRect.y + endRect.height}, new GUIContent(""), HotReloadWindowStyles.BarBackgroundStyle) && (entryType == EntryType.Child || entryType == EntryType.Foldout)) { ToggleEntry(alertEntry); } if (alertEntry.alertType != AlertType.Suggestion && HotReloadWindowStyles.windowScreenWidth > 400 && entryType != EntryType.Child) { using (new EditorGUILayout.HorizontalScope()) { var ago = (DateTime.Now - alertEntry.timestamp); GUI.Label(new Rect(startRect.x + startRect.width - 60, startRect.y, 80, 20), ago.TotalMinutes < 1 ? "now" : $"{(ago.TotalHours > 1 ? $"{Math.Floor(ago.TotalHours)} h " : string.Empty)}{ago.Minutes} min", HotReloadWindowStyles.TimestampStyle); } } GUILayout.Space(1f); } if (timelineType != TimelineType.Suggestions && HotReloadTimelineHelper.GetRunTabTimelineEventCount() > 40) { GUILayout.Space(3f); GUILayout.Label(Constants.Only40EntriesShown, HotReloadWindowStyles.EmptyListText); } } private static List _enabledFilters; private static List enabledFilters { get { if (_enabledFilters == null) { _enabledFilters = new List(); } if (HotReloadPrefs.RunTabUnsupportedChangesFilter && !_enabledFilters.Contains(AlertType.UnsupportedChange)) _enabledFilters.Add(AlertType.UnsupportedChange); if (!HotReloadPrefs.RunTabUnsupportedChangesFilter && _enabledFilters.Contains(AlertType.UnsupportedChange)) _enabledFilters.Remove(AlertType.UnsupportedChange); if (HotReloadPrefs.RunTabCompileErrorFilter && !_enabledFilters.Contains(AlertType.CompileError)) _enabledFilters.Add(AlertType.CompileError); if (!HotReloadPrefs.RunTabCompileErrorFilter && _enabledFilters.Contains(AlertType.CompileError)) _enabledFilters.Remove(AlertType.CompileError); if (HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter && !_enabledFilters.Contains(AlertType.PartiallySupportedChange)) _enabledFilters.Add(AlertType.PartiallySupportedChange); if (!HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter && _enabledFilters.Contains(AlertType.PartiallySupportedChange)) _enabledFilters.Remove(AlertType.PartiallySupportedChange); if (HotReloadPrefs.RunTabUndetectedPatchesFilter && !_enabledFilters.Contains(AlertType.UndetectedChange)) _enabledFilters.Add(AlertType.UndetectedChange); if (!HotReloadPrefs.RunTabUndetectedPatchesFilter && _enabledFilters.Contains(AlertType.UndetectedChange)) _enabledFilters.Remove(AlertType.UndetectedChange); if (HotReloadPrefs.RunTabAppliedPatchesFilter && !_enabledFilters.Contains(AlertType.AppliedChange)) _enabledFilters.Add(AlertType.AppliedChange); if (!HotReloadPrefs.RunTabAppliedPatchesFilter && _enabledFilters.Contains(AlertType.AppliedChange)) _enabledFilters.Remove(AlertType.AppliedChange); return _enabledFilters; } } private Vector2 suggestionsScroll; static GUILayoutOption[] timelineButtonOptions = new[] { GUILayout.Height(27), GUILayout.Width(100) }; internal static void RenderBars(HotReloadRunTabState currentState) { if (currentState.suggestionCount > 0) { GUILayout.Space(5f); using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.Section)) { using (new EditorGUILayout.VerticalScope()) { HotReloadPrefs.RunTabEventsSuggestionsFoldout = EditorGUILayout.Foldout(HotReloadPrefs.RunTabEventsSuggestionsFoldout, "", true, HotReloadWindowStyles.CustomFoldoutStyle); GUILayout.Space(-23); if (GUILayout.Button($"Suggestions ({currentState.suggestionCount.ToString()})", HotReloadWindowStyles.ClickableLabelBoldStyle, GUILayout.Height(27))) { HotReloadPrefs.RunTabEventsSuggestionsFoldout = !HotReloadPrefs.RunTabEventsSuggestionsFoldout; } if (HotReloadPrefs.RunTabEventsSuggestionsFoldout) { using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.Scroll)) { RenderEntries(TimelineType.Suggestions); } } } } } GUILayout.Space(5f); using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.Section)) { using (new EditorGUILayout.VerticalScope()) { HotReloadPrefs.RunTabEventsTimelineFoldout = EditorGUILayout.Foldout(HotReloadPrefs.RunTabEventsTimelineFoldout, "", true, HotReloadWindowStyles.CustomFoldoutStyle); GUILayout.Space(-23); if (GUILayout.Button("Timeline", HotReloadWindowStyles.ClickableLabelBoldStyle, timelineButtonOptions)) { HotReloadPrefs.RunTabEventsTimelineFoldout = !HotReloadPrefs.RunTabEventsTimelineFoldout; } if (HotReloadPrefs.RunTabEventsTimelineFoldout) { GUILayout.Space(-10); var noteShown = HotReloadTimelineHelper.GetRunTabTimelineEventCount() == 0 || !currentState.running; using (new EditorGUILayout.HorizontalScope()) { if (noteShown) { GUILayout.Space(2f); using (new EditorGUILayout.VerticalScope()) { GUILayout.Space(2f); string text; if (currentState.redeemStage != RedeemStage.None) { text = "Complete registration before using Hot Reload"; } else if (!currentState.running) { text = "Use the Start button to activate Hot Reload"; } else if (enabledFilters.Count < 4 && HotReloadTimelineHelper.EventsTimeline.Count != 0) { text = "Enable filters to see events"; } else { text = "Make code changes to see events"; } GUILayout.Label(text, HotReloadWindowStyles.EmptyListText); } GUILayout.FlexibleSpace(); } else { GUILayout.FlexibleSpace(); if (HotReloadTimelineHelper.EventsTimeline.Count > 0 && GUILayout.Button("Clear")) { HotReloadTimelineHelper.ClearEntries(); if (HotReloadWindow.Current) { HotReloadWindow.Current.Repaint(); } } GUILayout.Space(3); } } if (!noteShown) { GUILayout.Space(2f); using (new EditorGUILayout.VerticalScope()) { RenderEntries(TimelineType.Timeline); } } } } } } internal static void RenderConsumption(LoginStatusResponse loginStatus) { if (loginStatus == null) { return; } EditorGUILayout.Space(); EditorGUILayout.LabelField($"Hot Reload Limited", HotReloadWindowStyles.H3CenteredTitleStyle); EditorGUILayout.Space(); if (loginStatus.consumptionsUnavailableReason == ConsumptionsUnavailableReason.NetworkUnreachable) { EditorGUILayout.HelpBox("Something went wrong. Please check your internet connection.", MessageType.Warning); } else if (loginStatus.consumptionsUnavailableReason == ConsumptionsUnavailableReason.UnrecoverableError) { EditorGUILayout.HelpBox("Something went wrong. Please contact support if the issue persists.", MessageType.Error); } else if (loginStatus.freeSessionFinished) { var now = DateTime.UtcNow; var sessionRefreshesAt = (now.AddDays(1).Date - now).Add(TimeSpan.FromMinutes(5)); var sessionRefreshString = $"Next Session: {(sessionRefreshesAt.Hours > 0 ? $"{sessionRefreshesAt.Hours}h " : "")}{sessionRefreshesAt.Minutes}min"; HotReloadGUIHelper.HelpBox(sessionRefreshString, MessageType.Warning, fontSize: 11); } else if (loginStatus.freeSessionRunning && loginStatus.freeSessionEndTime != null) { var sessionEndsAt = loginStatus.freeSessionEndTime.Value - DateTime.Now; var sessionString = $"Daily Session: {(sessionEndsAt.Hours > 0 ? $"{sessionEndsAt.Hours}h " : "")}{sessionEndsAt.Minutes}min Left"; HotReloadGUIHelper.HelpBox(sessionString, MessageType.Info, fontSize: 11); } else if (loginStatus.freeSessionEndTime == null) { HotReloadGUIHelper.HelpBox("Daily Session: Make code changes to start", MessageType.Info, fontSize: 11); } } static bool _repaint; static bool _instantRepaint; static DateTime _lastRepaint; private EditorIndicationState.IndicationStatus _lastStatus; public override void Update() { if (EditorIndicationState.SpinnerActive) { _repaint = true; } if (EditorCodePatcher.DownloadRequired) { _repaint = true; } if (EditorIndicationState.IndicationIconPath == Spinner.SpinnerIconPath) { _repaint = true; } try { // workaround: hovering over non-buttons doesn't repain by default if (EditorWindow.mouseOverWindow == HotReloadWindow.Current) { _repaint = true; } if (EditorWindow.mouseOverWindow && EditorWindow.mouseOverWindow?.GetType() == typeof(PopupWindow) && HotReloadEventPopup.I.open ) { _repaint = true; } } catch (NullReferenceException) { // Unity randomly throws nullrefs when EditorWindow.mouseOverWindow gets accessed } if (_repaint && DateTime.UtcNow - _lastRepaint > TimeSpan.FromMilliseconds(33)) { _repaint = false; _instantRepaint = true; } // repaint on status change var status = EditorIndicationState.CurrentIndicationStatus; if (_lastStatus != status) { _lastStatus = status; _instantRepaint = true; } if (_instantRepaint) { Repaint(); HotReloadEventPopup.I.Repaint(); _instantRepaint = false; _repaint = false; _lastRepaint = DateTime.UtcNow; } } public static void RepaintInstant() { _instantRepaint = true; } private void RenderRecompileButton() { string recompileText = HotReloadWindowStyles.windowScreenWidth > Constants.RecompileButtonTextHideWidth ? " Recompile" : ""; var recompileButton = new GUIContent(recompileText, GUIHelper.GetInvertibleIcon(InvertibleIcon.Recompile)); if (!GUILayout.Button(recompileButton, HotReloadWindowStyles.RecompileButton)) { return; } RecompileWithChecks(); } public static void RecompileWithChecks() { var firstDialoguePass = HotReloadPrefs.RecompileDialogueShown || EditorUtility.DisplayDialog( title: "Hot Reload auto-applies changes", message: "Using the Recompile button is only necessary when Hot Reload fails to apply your changes. \n\nDo you wish to proceed?", ok: "Recompile", cancel: "Not now"); HotReloadPrefs.RecompileDialogueShown = true; if (!firstDialoguePass) { return; } if (!ConfirmExitPlaymode("Using the Recompile button will stop Play Mode.\n\nDo you wish to proceed?")) { return; } Recompile(); } #if UNITY_2020_1_OR_NEWER public static void SwitchToDebugMode() { CompilationPipeline.codeOptimization = CodeOptimization.Debug; HotReloadRunTab.Recompile(); HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods); } #endif public static bool ConfirmExitPlaymode(string message) { return !Application.isPlaying || EditorUtility.DisplayDialog( title: "Stop Play Mode and Recompile?", message: message, ok: "Stop and Recompile", cancel: "Cancel"); } public static bool recompiling; public static void Recompile() { recompiling = true; EditorApplication.isPlaying = false; CompileMethodDetourer.Reset(); AssetDatabase.Refresh(); // This forces the recompilation if no changes were made. // This is better UX because otherwise the recompile button is unresponsive // which can be extra annoying if there are compile error entries in the list if (!EditorApplication.isCompiling) { CompilationPipeline.RequestScriptCompilation(); } } private void RenderIndicationButtons() { if (currentState.requestingDownloadAndRun || currentState.starting || currentState.stopping || currentState.redeemStage != RedeemStage.None) { return; } if (!currentState.running && (currentState.startupProgress?.Item1 ?? 0) == 0) { string startText = HotReloadWindowStyles.windowScreenWidth > Constants.StartButtonTextHideWidth ? " Start" : ""; if (GUILayout.Button(new GUIContent(startText, GUIHelper.GetInvertibleIcon(InvertibleIcon.Start)), HotReloadWindowStyles.StartButton)) { EditorCodePatcher.DownloadAndRun().Forget(); } } else if (currentState.running && !currentState.starting) { if (HotReloadWindowStyles.windowScreenWidth > 150) { RenderRecompileButton(); } string stopText = HotReloadWindowStyles.windowScreenWidth > Constants.StartButtonTextHideWidth ? " Stop" : ""; if (GUILayout.Button(new GUIContent(stopText, GUIHelper.GetInvertibleIcon(InvertibleIcon.Stop)), HotReloadWindowStyles.StopButton)) { if (!EditorCodePatcher.StoppedServerRecently()) { EditorCodePatcher.StopCodePatcher().Forget(); } } } } void RenderIndicationPanel() { using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionInnerBox)) { RenderIndication(); if (HotReloadWindowStyles.windowScreenWidth > Constants.IndicationTextHideWidth) { GUILayout.FlexibleSpace(); } RenderIndicationButtons(); if (HotReloadWindowStyles.windowScreenWidth <= Constants.IndicationTextHideWidth) { GUILayout.FlexibleSpace(); } } if (currentState.requestingDownloadAndRun || currentState.starting) { RenderProgressBar(); } if (HotReloadWindowStyles.windowScreenWidth > Constants.ConsumptionsHideWidth && HotReloadWindowStyles.windowScreenHeight > Constants.ConsumptionsHideHeight ) { RenderLicenseInfo(currentState); } } internal static void RenderLicenseInfo(HotReloadRunTabState currentState) { var showRedeem = currentState.redeemStage != RedeemStage.None; var showConsumptions = ShouldRenderConsumption(currentState); if (!showConsumptions && !showRedeem) { return; } using (new EditorGUILayout.VerticalScope()) { // space needed only for consumptions because of Stop/Start button's margin if (showConsumptions) { GUILayout.Space(6); } using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.Section)) { if (showRedeem) { RedeemLicenseHelper.I.RenderStage(currentState); } else { RenderConsumption(currentState.loginStatus); GUILayout.Space(10); RenderLicenseInfo(currentState, currentState.loginStatus); RenderLicenseButtons(currentState); GUILayout.Space(10); } } GUILayout.Space(6); } } private Spinner _spinner = new Spinner(85); private void RenderIndication() { using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.IndicationBox)) { // icon box if (HotReloadWindowStyles.windowScreenWidth <= Constants.IndicationTextHideWidth) { GUILayout.FlexibleSpace(); } using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.IndicationHelpBox)) { var text = HotReloadWindowStyles.windowScreenWidth > Constants.IndicationTextHideWidth ? $" {currentState.indicationStatusText}" : ""; if (currentState.indicationIconPath == Spinner.SpinnerIconPath) { GUILayout.Label(new GUIContent(text, _spinner.GetIcon()), style: HotReloadWindowStyles.IndicationIcon); } else if (currentState.indicationIconPath != null) { var style = HotReloadWindowStyles.IndicationIcon; if (HotReloadTimelineHelper.alertIconString.ContainsValue(currentState.indicationIconPath)) { style = HotReloadWindowStyles.IndicationAlertIcon; } GUILayout.Label(new GUIContent(text, GUIHelper.GetLocalIcon(currentState.indicationIconPath)), style); } } } } static GUIStyle _openSettingsStyle; static GUIStyle openSettingsStyle => _openSettingsStyle ?? (_openSettingsStyle = new GUIStyle(GUI.skin.button) { fontStyle = FontStyle.Normal, fixedHeight = 25, }); static GUILayoutOption[] _bigButtonHeight; public static GUILayoutOption[] bigButtonHeight => _bigButtonHeight ?? (_bigButtonHeight = new [] {GUILayout.Height(25)}); private static GUIContent indieLicenseContent; private static GUIContent businessLicenseContent; internal static void RenderLicenseStatusInfo(HotReloadRunTabState currentState, LoginStatusResponse loginStatus, bool allowHide = true, bool verbose = false) { string message = null; MessageType messageType = default(MessageType); Action customGUI = null; GUIContent content = null; if (loginStatus == null) { // no info } else if (loginStatus.lastLicenseError != null) { messageType = !loginStatus.freeSessionFinished ? MessageType.Warning : MessageType.Error; message = GetMessageFromError(currentState, loginStatus.lastLicenseError); } else if (loginStatus.isTrial && !PackageConst.IsAssetStoreBuild) { message = $"Using Trial license, valid until {loginStatus.licenseExpiresAt.ToShortDateString()}"; messageType = MessageType.Info; } else if (loginStatus.isIndieLicense) { if (verbose) { message = " Indie license active"; messageType = MessageType.Info; customGUI = () => { if (loginStatus.licenseExpiresAt.Date != DateTime.MaxValue.Date) { EditorGUILayout.LabelField($"License will renew on {loginStatus.licenseExpiresAt.ToShortDateString()}."); EditorGUILayout.Space(); } using (new GUILayout.HorizontalScope()) { HotReloadAboutTab.manageLicenseButton.OnGUI(); HotReloadAboutTab.manageAccountButton.OnGUI(); } EditorGUILayout.Space(); }; if (indieLicenseContent == null) { indieLicenseContent = new GUIContent(message, EditorGUIUtility.FindTexture("TestPassed")); } content = indieLicenseContent; } } else if (loginStatus.isBusinessLicense) { if (verbose) { message = " Business license active"; messageType = MessageType.Info; if (businessLicenseContent == null) { businessLicenseContent = new GUIContent(message, EditorGUIUtility.FindTexture("TestPassed")); } content = businessLicenseContent; customGUI = () => { using (new GUILayout.HorizontalScope()) { HotReloadAboutTab.manageLicenseButton.OnGUI(); HotReloadAboutTab.manageAccountButton.OnGUI(); } EditorGUILayout.Space(); }; } } if (messageType != MessageType.Info && HotReloadPrefs.ErrorHidden && allowHide) { return; } if (message != null) { if (messageType != MessageType.Info) { using(new EditorGUILayout.HorizontalScope()) { EditorGUILayout.HelpBox(message, messageType); var style = HotReloadWindowStyles.HideButtonStyle; if (Event.current.type == EventType.Repaint) { style.fixedHeight = GUILayoutUtility.GetLastRect().height; } if (allowHide) { if (GUILayout.Button("Hide", style)) { HotReloadPrefs.ErrorHidden = true; } } } } else if (content != null) { EditorGUILayout.LabelField(content); EditorGUILayout.Space(); } else { EditorGUILayout.LabelField(message); EditorGUILayout.Space(); } customGUI?.Invoke(); } } const string assetStoreProInfo = "Unity Pro/Enterprise users from company with your number of employees require a Business license. Please upgrade your license on our website."; internal static void RenderBusinessLicenseInfo(GUIStyle style) { GUILayout.Space(8); using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.HelpBox(assetStoreProInfo, MessageType.Info); if (Event.current.type == EventType.Repaint) { style.fixedHeight = GUILayoutUtility.GetLastRect().height; } if (GUILayout.Button("Upgrade", style)) { Application.OpenURL(Constants.ProductPurchaseBusinessURL); } } } internal static void RenderIndieLicenseInfo(GUIStyle style) { string message; if (EditorCodePatcher.licenseType == UnityLicenseType.UnityPersonalPlus) { message = "Unity Plus users require an Indie license. Please upgrade your license on our website."; } else if (EditorCodePatcher.licenseType == UnityLicenseType.UnityPro) { message = "Unity Pro/Enterprise users from company with your number of employees require an Indie license. Please upgrade your license on our website."; } else { return; } GUILayout.Space(8); using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.HelpBox(message, MessageType.Info); if (Event.current.type == EventType.Repaint) { style.fixedHeight = GUILayoutUtility.GetLastRect().height; } if (GUILayout.Button("Upgrade", style)) { Application.OpenURL(Constants.ProductPurchaseURL); } } } const string GetLicense = "Get License"; const string ContactSupport = "Contact Support"; const string UpgradeLicense = "Upgrade License"; const string ManageLicense = "Manage License"; internal static Dictionary _licenseErrorData; internal static Dictionary LicenseErrorData => _licenseErrorData ?? (_licenseErrorData = new Dictionary { { "DeviceNotLicensedException", new LicenseErrorData(description: "Another device is using your license. Please reach out to customer support for assistance.", showSupportButton: true, supportButtonText: ContactSupport) }, { "DeviceBlacklistedException", new LicenseErrorData(description: "You device has been blacklisted.") }, { "DateHeaderInvalidException", new LicenseErrorData(description: $"Your license is not working because your computer's clock is incorrect. Please set the clock to the correct time to restore your license.") }, { "DateTimeCheatingException", new LicenseErrorData(description: $"Your license is not working because your computer's clock is incorrect. Please set the clock to the correct time to restore your license.") }, { "LicenseActivationException", new LicenseErrorData(description: "An error has occured while activating your license. Please contact customer support for assistance.", showSupportButton: true, supportButtonText: ContactSupport) }, { "LicenseDeletedException", new LicenseErrorData(description: $"Your license has been deleted. Please contact customer support for assistance.", showBuyButton: true, buyButtonText: GetLicense, showSupportButton: true, supportButtonText: ContactSupport) }, { "LicenseDisabledException", new LicenseErrorData(description: $"Your license has been disabled. Please contact customer support for assistance.", showBuyButton: true, buyButtonText: GetLicense, showSupportButton: true, supportButtonText: ContactSupport) }, { "LicenseExpiredException", new LicenseErrorData(description: $"Your license has expired. Please renew your license subscription using the 'Upgrade License' button below and login with your email/password to activate your license.", showBuyButton: true, buyButtonText: UpgradeLicense, showManageLicenseButton: true, manageLicenseButtonText: ManageLicense) }, { "LicenseInactiveException", new LicenseErrorData(description: $"Your license is currenty inactive. Please login with your email/password to activate your license.") }, { "LocalLicenseException", new LicenseErrorData(description: $"Your license file was damaged or corrupted. Please login with your email/password to refresh your license file.") }, // Note: obsolete { "MissingParametersException", new LicenseErrorData(description: "An account already exists for this device. Please login with your existing email/password.", showBuyButton: true, buyButtonText: GetLicense) }, { "NetworkException", new LicenseErrorData(description: "There is an issue connecting to our servers. Please check your internet connection or contact customer support if the issue persists.", showSupportButton: true, supportButtonText: ContactSupport) }, { "TrialLicenseExpiredException", new LicenseErrorData(description: $"Your trial has expired. Activate a license with unlimited usage or continue using the Free version. View available plans on our website.", showBuyButton: true, buyButtonText: UpgradeLicense) }, { "InvalidCredentialException", new LicenseErrorData(description: "Incorrect email/password. You can find your initial password in the sign-up email.") }, // Note: activating free trial with email is not supported anymore. This error shouldn't happen which is why we should rather user the fallback // { "LicenseNotFoundException", new LicenseErrorData(description: "The account you're trying to access doesn't seem to exist yet. Please enter your email address to create a new account and receive a trial license.", showLoginButton: true, loginButtonText: CreateAccount) }, { "LicenseIncompatibleException", new LicenseErrorData(description: "Please upgrade your license to continue using hotreload with Unity Pro.", showManageLicenseButton: true, manageLicenseButtonText: ManageLicense) }, }); internal static LicenseErrorData defaultLicenseErrorData = new LicenseErrorData(description: "We apologize, an error happened while verifying your license. Please reach out to customer support for assistance.", showSupportButton: true, supportButtonText: ContactSupport); internal static string GetMessageFromError(HotReloadRunTabState currentState, string error) { if (PackageConst.IsAssetStoreBuild && error == "TrialLicenseExpiredException") { return assetStoreProInfo; } return GetLicenseErrorDataOrDefault(currentState, error).description; } internal static LicenseErrorData GetLicenseErrorDataOrDefault(HotReloadRunTabState currentState, string error) { if (currentState.loginStatus?.isFree == true) { return default(LicenseErrorData); } if (currentState.loginStatus == null || string.IsNullOrEmpty(error) && (!currentState.loginStatus.isLicensed || currentState.loginStatus.isTrial)) { return new LicenseErrorData(null, showBuyButton: true, buyButtonText: GetLicense); } if (string.IsNullOrEmpty(error)) { return default(LicenseErrorData); } if (!LicenseErrorData.ContainsKey(error)) { return defaultLicenseErrorData; } return LicenseErrorData[error]; } internal static void RenderBuyLicenseButton(string buyLicenseButton) { OpenURLButton.Render(buyLicenseButton, Constants.ProductPurchaseURL); } static void RenderLicenseActionButtons(HotReloadRunTabState currentState) { var errInfo = GetLicenseErrorDataOrDefault(currentState, currentState.loginStatus?.lastLicenseError); if (errInfo.showBuyButton || errInfo.showManageLicenseButton) { using(new EditorGUILayout.HorizontalScope()) { if (errInfo.showBuyButton) { RenderBuyLicenseButton(errInfo.buyButtonText); } if (errInfo.showManageLicenseButton && !HotReloadPrefs.ErrorHidden) { OpenURLButton.Render(errInfo.manageLicenseButtonText, Constants.ManageLicenseURL); } } } if (errInfo.showLoginButton && GUILayout.Button(errInfo.loginButtonText, openSettingsStyle)) { // show license section HotReloadWindow.Current.SelectTab(typeof(HotReloadSettingsTab)); HotReloadWindow.Current.SettingsTab.FocusLicenseFoldout(); } if (errInfo.showSupportButton && !HotReloadPrefs.ErrorHidden) { OpenURLButton.Render(errInfo.supportButtonText, Constants.ContactURL); } if (currentState.loginStatus?.lastLicenseError != null) { HotReloadAboutTab.reportIssueButton.OnGUI(); } } internal static void RenderLicenseInfo(HotReloadRunTabState currentState, LoginStatusResponse loginStatus, bool verbose = false, bool allowHide = true, string overrideActionButton = null, bool showConsumptions = false) { HotReloadPrefs.ShowLogin = EditorGUILayout.Foldout(HotReloadPrefs.ShowLogin, "Hot Reload License", true, HotReloadWindowStyles.FoldoutStyle); if (HotReloadPrefs.ShowLogin) { EditorGUILayout.Space(); if ((loginStatus?.isLicensed != true && showConsumptions) && !(loginStatus == null || loginStatus.isFree)) { RenderConsumption(loginStatus); } RenderLicenseStatusInfo(currentState, loginStatus: loginStatus, allowHide: allowHide, verbose: verbose); RenderLicenseInnerPanel(currentState, overrideActionButton: overrideActionButton); EditorGUILayout.Space(); EditorGUILayout.Space(); } } internal void RenderPromoCodes() { HotReloadPrefs.ShowPromoCodes = EditorGUILayout.Foldout(HotReloadPrefs.ShowPromoCodes, "Promo Codes", true, HotReloadWindowStyles.FoldoutStyle); if (!HotReloadPrefs.ShowPromoCodes) { return; } if (promoCodeActivatedThisSession) { EditorGUILayout.HelpBox($"Your promo code has been successfully activated. Free trial has been extended by 3 months.", MessageType.Info); } else { if (promoCodeError != null && promoCodeErrorType != MessageType.None) { EditorGUILayout.HelpBox(promoCodeError, promoCodeErrorType); } EditorGUILayout.LabelField("Promo code"); _pendingPromoCode = EditorGUILayout.TextField(_pendingPromoCode); EditorGUILayout.Space(); using (new EditorGUI.DisabledScope(_requestingActivatePromoCode)) { if (GUILayout.Button("Activate promo code", HotReloadRunTab.bigButtonHeight)) { RequestActivatePromoCode().Forget(); } } } EditorGUILayout.Space(); EditorGUILayout.Space(); } private async Task RequestActivatePromoCode() { _requestingActivatePromoCode = true; try { var resp = await RequestHelper.RequestActivatePromoCode(_pendingPromoCode); if (resp != null && resp.error == null) { promoCodeActivatedThisSession = true; } else { var requestError = resp?.error ?? "Network error"; var errorType = ToErrorType(requestError); promoCodeError = ToPrettyErrorMessage(errorType); promoCodeErrorType = ToMessageType(errorType); } } finally { _requestingActivatePromoCode = false; } } PromoCodeErrorType ToErrorType(string error) { switch (error) { case "Input is missing": return PromoCodeErrorType.MISSING_INPUT; case "only POST is supported": return PromoCodeErrorType.INVALID_HTTP_METHOD; case "body is not a valid json": return PromoCodeErrorType.BODY_INVALID; case "Promo code is not found": return PromoCodeErrorType.PROMO_CODE_NOT_FOUND; case "Promo code already claimed": return PromoCodeErrorType.PROMO_CODE_CLAIMED; case "Promo code expired": return PromoCodeErrorType.PROMO_CODE_EXPIRED; case "License not found": return PromoCodeErrorType.LICENSE_NOT_FOUND; case "License is not a trial": return PromoCodeErrorType.LICENSE_NOT_TRIAL; case "License already extended": return PromoCodeErrorType.LICENSE_ALREADY_EXTENDED; case "conditionalCheckFailed": return PromoCodeErrorType.CONDITIONAL_CHECK_FAILED; } if (error.Contains("Updating License Failed with error")) { return PromoCodeErrorType.UPDATING_LICENSE_FAILED; } else if (error.Contains("Unknown exception")) { return PromoCodeErrorType.UNKNOWN_EXCEPTION; } else if (error.Contains("Unsupported path")) { return PromoCodeErrorType.UNSUPPORTED_PATH; } return PromoCodeErrorType.NONE; } string ToPrettyErrorMessage(PromoCodeErrorType errorType) { var defaultMsg = "We apologize, an error happened while activating your promo code. Please reach out to customer support for assistance."; switch (errorType) { case PromoCodeErrorType.MISSING_INPUT: case PromoCodeErrorType.INVALID_HTTP_METHOD: case PromoCodeErrorType.BODY_INVALID: case PromoCodeErrorType.UNKNOWN_EXCEPTION: case PromoCodeErrorType.UNSUPPORTED_PATH: case PromoCodeErrorType.LICENSE_NOT_FOUND: case PromoCodeErrorType.UPDATING_LICENSE_FAILED: case PromoCodeErrorType.LICENSE_NOT_TRIAL: return defaultMsg; case PromoCodeErrorType.PROMO_CODE_NOT_FOUND: return "Your promo code is invalid. Please ensure that you have entered the correct promo code."; case PromoCodeErrorType.PROMO_CODE_CLAIMED: return "Your promo code has already been used."; case PromoCodeErrorType.PROMO_CODE_EXPIRED: return "Your promo code has expired."; case PromoCodeErrorType.LICENSE_ALREADY_EXTENDED: return "Your license has already been activated with a promo code. Only one promo code activation per license is allowed."; case PromoCodeErrorType.CONDITIONAL_CHECK_FAILED: return "We encountered an error while activating your promo code. Please try again. If the issue persists, please contact our customer support team for assistance."; case PromoCodeErrorType.NONE: return "There is an issue connecting to our servers. Please check your internet connection or contact customer support if the issue persists."; default: return defaultMsg; } } MessageType ToMessageType(PromoCodeErrorType errorType) { switch (errorType) { case PromoCodeErrorType.MISSING_INPUT: return MessageType.Error; case PromoCodeErrorType.INVALID_HTTP_METHOD: return MessageType.Error; case PromoCodeErrorType.BODY_INVALID: return MessageType.Error; case PromoCodeErrorType.PROMO_CODE_NOT_FOUND: return MessageType.Warning; case PromoCodeErrorType.PROMO_CODE_CLAIMED: return MessageType.Warning; case PromoCodeErrorType.PROMO_CODE_EXPIRED: return MessageType.Warning; case PromoCodeErrorType.LICENSE_NOT_FOUND: return MessageType.Error; case PromoCodeErrorType.LICENSE_NOT_TRIAL: return MessageType.Error; case PromoCodeErrorType.LICENSE_ALREADY_EXTENDED: return MessageType.Warning; case PromoCodeErrorType.UPDATING_LICENSE_FAILED: return MessageType.Error; case PromoCodeErrorType.CONDITIONAL_CHECK_FAILED: return MessageType.Error; case PromoCodeErrorType.UNKNOWN_EXCEPTION: return MessageType.Error; case PromoCodeErrorType.UNSUPPORTED_PATH: return MessageType.Error; case PromoCodeErrorType.NONE: return MessageType.Error; default: return MessageType.Error; } } public static void RenderLicenseButtons(HotReloadRunTabState currentState) { RenderLicenseActionButtons(currentState); } internal static void RenderLicenseInnerPanel(HotReloadRunTabState currentState, string overrideActionButton = null, bool renderLogout = true) { EditorGUILayout.LabelField("Email"); GUI.SetNextControlName("email"); _pendingEmail = EditorGUILayout.TextField(string.IsNullOrEmpty(_pendingEmail) ? HotReloadPrefs.LicenseEmail : _pendingEmail); _pendingEmail = _pendingEmail.Trim(); EditorGUILayout.LabelField("Password"); GUI.SetNextControlName("password"); _pendingPassword = EditorGUILayout.PasswordField(string.IsNullOrEmpty(_pendingPassword) ? HotReloadPrefs.LicensePassword : _pendingPassword); RenderSwitchAuthMode(); var e = Event.current; using(new EditorGUI.DisabledScope(currentState.requestingLoginInfo)) { var btnLabel = overrideActionButton; if (String.IsNullOrEmpty(overrideActionButton)) { btnLabel = "Login"; } using (new EditorGUILayout.HorizontalScope()) { var focusedControl = GUI.GetNameOfFocusedControl(); if (GUILayout.Button(btnLabel, bigButtonHeight) || (focusedControl == "email" || focusedControl == "password") && e.type == EventType.KeyUp && (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) ) { var error = ValidateEmail(_pendingEmail); if (!string.IsNullOrEmpty(error)) { _activateInfoMessage = new Tuple(error, MessageType.Warning); } else if (string.IsNullOrEmpty(_pendingPassword)) { _activateInfoMessage = new Tuple("Please enter your password.", MessageType.Warning); } else { HotReloadWindow.Current.SelectTab(typeof(HotReloadRunTab)); _activateInfoMessage = null; if (RedeemLicenseHelper.I.RedeemStage == RedeemStage.Login) { RedeemLicenseHelper.I.FinishRegistration(RegistrationOutcome.Indie); } if (!EditorCodePatcher.RequestingDownloadAndRun && !EditorCodePatcher.Running) { LoginOnDownloadAndRun(new LoginData(email: _pendingEmail, password: _pendingPassword)).Forget(); } else { EditorCodePatcher.RequestLogin(_pendingEmail, _pendingPassword).Forget(); } } } if (renderLogout) { RenderLogout(currentState); } } } if (_activateInfoMessage != null && (e.type == EventType.Layout || e.type == EventType.Repaint)) { EditorGUILayout.HelpBox(_activateInfoMessage.Item1, _activateInfoMessage.Item2); } } public static string ValidateEmail(string email) { if (string.IsNullOrEmpty(email)) { return "Please enter your email address."; } else if (!EditorWindowHelper.IsValidEmailAddress(email)) { return "Please enter a valid email address."; } else if (email.Contains("+")) { return "Mail extensions (in a form of 'username+suffix@example.com') are not supported yet. Please provide your original email address (such as 'username@example.com' without '+suffix' part) as we're working on resolving this issue."; } return null; } public static void RenderLogout(HotReloadRunTabState currentState) { if (currentState.loginStatus?.isLicensed != true) { return; } if (GUILayout.Button("Logout", bigButtonHeight)) { HotReloadWindow.Current.SelectTab(typeof(HotReloadRunTab)); if (!EditorCodePatcher.RequestingDownloadAndRun && !EditorCodePatcher.Running) { LogoutOnDownloadAndRun().Forget(); } else { RequestLogout().Forget(); } } } async static Task LoginOnDownloadAndRun(LoginData loginData = null) { var ok = await EditorCodePatcher.DownloadAndRun(loginData); if (ok && loginData != null) { HotReloadPrefs.ErrorHidden = false; HotReloadPrefs.LicenseEmail = loginData.email; HotReloadPrefs.LicensePassword = loginData.password; } } async static Task LogoutOnDownloadAndRun() { var ok = await EditorCodePatcher.DownloadAndRun(); if (!ok) { return; } await RequestLogout(); } private async static Task RequestLogout() { int i = 0; while (!EditorCodePatcher.Running && i < 100) { await Task.Delay(100); i++; } var resp = await RequestHelper.RequestLogout(); if (!EditorCodePatcher.RequestingLoginInfo && resp != null) { EditorCodePatcher.HandleStatus(resp); } } private static void RenderSwitchAuthMode() { var color = EditorGUIUtility.isProSkin ? new Color32(0x3F, 0x9F, 0xFF, 0xFF) : new Color32(0x0F, 0x52, 0xD7, 0xFF); if (HotReloadGUIHelper.LinkLabel("Forgot password?", 12, FontStyle.Normal, TextAnchor.MiddleLeft, color)) { if (EditorUtility.DisplayDialog("Recover password", "Use company code 'naughtycult' and the email you signed up with in order to recover your account.", "Open in browser", "Cancel")) { Application.OpenURL(Constants.ForgotPasswordURL); } } } Texture2D _greenTextureLight; Texture2D _greenTextureDark; Texture2D GreenTexture => EditorGUIUtility.isProSkin ? _greenTextureDark ? _greenTextureDark : (_greenTextureDark = MakeTexture(0.5f)) : _greenTextureLight ? _greenTextureLight : (_greenTextureLight = MakeTexture(0.85f)); private void RenderProgressBar() { if (currentState.downloadRequired && !currentState.downloadStarted) { return; } using(var scope = new EditorGUILayout.VerticalScope(HotReloadWindowStyles.MiddleCenterStyle)) { float progress; var bg = HotReloadWindowStyles.ProgressBarBarStyle.normal.background; try { HotReloadWindowStyles.ProgressBarBarStyle.normal.background = GreenTexture; var barRect = scope.rect; barRect.height = 25; if (currentState.downloadRequired) { barRect.width = barRect.width - 65; using (new EditorGUILayout.HorizontalScope()) { progress = EditorCodePatcher.DownloadProgress; EditorGUI.ProgressBar(barRect, Mathf.Clamp(progress, 0f, 1f), ""); if (GUI.Button(new Rect(barRect) { x = barRect.x + barRect.width + 5, height = barRect.height, width = 60 }, new GUIContent(" Info", GUIHelper.GetLocalIcon("alert_info")))) { Application.OpenURL(Constants.AdditionalContentURL); } } } else { progress = EditorCodePatcher.Stopping ? 1 : Mathf.Clamp(EditorCodePatcher.StartupProgress?.Item1 ?? 0f, 0f, 1f); EditorGUI.ProgressBar(barRect, progress, ""); } GUILayout.Space(barRect.height); } finally { HotReloadWindowStyles.ProgressBarBarStyle.normal.background = bg; } } } private Texture2D MakeTexture(float maxHue) { var width = 11; var height = 11; Color[] pix = new Color[width * height]; for (int y = 0; y < height; y++) { var middle = Math.Ceiling(height / (double)2); var maxGreen = maxHue; var yCoord = y + 1; var green = maxGreen - Math.Abs(yCoord - middle) * 0.02; for (int x = 0; x < width; x++) { pix[y * width + x] = new Color(0.1f, (float)green, 0.1f, 1.0f); } } var result = new Texture2D(width, height); result.SetPixels(pix); result.Apply(); return result; } /* [MenuItem("codepatcher/restart")] public static void TestRestart() { CodePatcherCLI.Restart(Application.dataPath, false); } */ } internal static class HotReloadGUIHelper { public static bool LinkLabel(string labelText, int fontSize, FontStyle fontStyle, TextAnchor alignment, Color? color = null) { var stl = EditorStyles.label; // copy var origSize = stl.fontSize; var origStyle = stl.fontStyle; var origAnchor = stl.alignment; var origColor = stl.normal.textColor; // temporarily modify the built-in style stl.fontSize = fontSize; stl.fontStyle = fontStyle; stl.alignment = alignment; stl.normal.textColor = color ?? origColor; stl.active.textColor = color ?? origColor; stl.focused.textColor = color ?? origColor; stl.hover.textColor = color ?? origColor; try { return GUILayout.Button(labelText, stl); } finally{ // set the editor style (stl) back to normal stl.fontSize = origSize; stl.fontStyle = origStyle; stl.alignment = origAnchor; stl.normal.textColor = origColor; stl.active.textColor = origColor; stl.focused.textColor = origColor; stl.hover.textColor = origColor; } } public static void HelpBox(string message, MessageType type, int fontSize) { var _fontSize = EditorStyles.helpBox.fontSize; try { EditorStyles.helpBox.fontSize = fontSize; EditorGUILayout.HelpBox(message, type); } finally { EditorStyles.helpBox.fontSize = _fontSize; } } } internal enum PromoCodeErrorType { NONE, MISSING_INPUT, INVALID_HTTP_METHOD, BODY_INVALID, PROMO_CODE_NOT_FOUND, PROMO_CODE_CLAIMED, PROMO_CODE_EXPIRED, LICENSE_NOT_FOUND, LICENSE_NOT_TRIAL, LICENSE_ALREADY_EXTENDED, UPDATING_LICENSE_FAILED, CONDITIONAL_CHECK_FAILED, UNKNOWN_EXCEPTION, UNSUPPORTED_PATH, } internal class LoginData { public readonly string email; public readonly string password; public LoginData(string email, string password) { this.email = email; this.password = password; } } }