2025-07-08 10:46:31 +00:00
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 < TextAsset > ( 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 < float , string > 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 < float , string > 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 < string , MessageType > _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 < AlertEntry > 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 < AlertType > _enabledFilters ;
private static List < AlertType > enabledFilters {
get {
if ( _enabledFilters = = null ) {
_enabledFilters = new List < AlertType > ( ) ;
}
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 < string , LicenseErrorData > _licenseErrorData ;
internal static Dictionary < string , LicenseErrorData > LicenseErrorData = > _licenseErrorData ? ? ( _licenseErrorData = new Dictionary < string , LicenseErrorData > {
{ "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 < string , MessageType > ( error , MessageType . Warning ) ;
} else if ( string . IsNullOrEmpty ( _pendingPassword ) ) {
_activateInfoMessage = new Tuple < string , MessageType > ( "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 ;
}
}
}