\d+))?" +
+ @"(\-(?[0-9A-Za-z\-\.]+))?" +
+ @"(\+(?[0-9A-Za-z\-\.]+))?$",
+#if NETSTANDARD
+ RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);
+#else
+ RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+#endif
+
+#if !NETSTANDARD
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ ///
+ ///
+ private SemVersion(SerializationInfo info, StreamingContext context)
+ {
+ if (info == null) throw new ArgumentNullException("info");
+ var semVersion = Parse(info.GetString("SemVersion"));
+ Major = semVersion.Major;
+ Minor = semVersion.Minor;
+ Patch = semVersion.Patch;
+ Prerelease = semVersion.Prerelease;
+ Build = semVersion.Build;
+ }
+#endif
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The major version.
+ /// The minor version.
+ /// The patch version.
+ /// The prerelease version (eg. "alpha").
+ /// The build eg ("nightly.232").
+ public SemVersion(int major, int minor = 0, int patch = 0, string prerelease = "", string build = "")
+ {
+ this.Major = major;
+ this.Minor = minor;
+ this.Patch = patch;
+
+ this.Prerelease = prerelease ?? "";
+ this.Build = build ?? "";
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The that is used to initialize
+ /// the Major, Minor, Patch and Build properties.
+ public SemVersion(Version version)
+ {
+ if (version == null)
+ throw new ArgumentNullException("version");
+
+ this.Major = version.Major;
+ this.Minor = version.Minor;
+
+ if (version.Revision >= 0)
+ {
+ this.Patch = version.Revision;
+ }
+
+ this.Prerelease = String.Empty;
+
+ if (version.Build > 0)
+ {
+ this.Build = version.Build.ToString();
+ }
+ else
+ {
+ this.Build = String.Empty;
+ }
+ }
+
+ ///
+ /// Parses the specified string to a semantic version.
+ ///
+ /// The version string.
+ /// If set to true minor and patch version are required, else they default to 0.
+ /// The SemVersion object.
+ /// When a invalid version string is passed.
+ public static SemVersion Parse(string version, bool strict = false)
+ {
+ var match = parseEx.Match(version);
+ if (!match.Success)
+ throw new ArgumentException("Invalid version.", "version");
+
+#if NETSTANDARD
+ var major = int.Parse(match.Groups["major"].Value);
+#else
+ var major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);
+#endif
+
+ var minorMatch = match.Groups["minor"];
+ int minor = 0;
+ if (minorMatch.Success)
+ {
+#if NETSTANDARD
+ minor = int.Parse(minorMatch.Value);
+#else
+ minor = int.Parse(minorMatch.Value, CultureInfo.InvariantCulture);
+#endif
+ }
+ else if (strict)
+ {
+ throw new InvalidOperationException(Translations.Utility.InvalidVersionNoMinor);
+ }
+
+ var patchMatch = match.Groups["patch"];
+ int patch = 0;
+ if (patchMatch.Success)
+ {
+#if NETSTANDARD
+ patch = int.Parse(patchMatch.Value);
+#else
+ patch = int.Parse(patchMatch.Value, CultureInfo.InvariantCulture);
+#endif
+ }
+ else if (strict)
+ {
+ throw new InvalidOperationException(Translations.Utility.InvalidVersionNoPatch);
+ }
+
+ var prerelease = match.Groups["pre"].Value;
+ var build = match.Groups["build"].Value;
+
+ return new SemVersion(major, minor, patch, prerelease, build);
+ }
+
+ ///
+ /// Parses the specified string to a semantic version.
+ ///
+ /// The version string.
+ /// When the method returns, contains a SemVersion instance equivalent
+ /// to the version string passed in, if the version string was valid, or null if the
+ /// version string was not valid.
+ /// If set to true minor and patch version are required, else they default to 0.
+ /// False when a invalid version string is passed, otherwise true.
+ public static bool TryParse(string version, out SemVersion semver, bool strict = false)
+ {
+ try
+ {
+ semver = Parse(version, strict);
+ return true;
+ }
+ catch (Exception)
+ {
+ semver = null;
+ return false;
+ }
+ }
+
+ ///
+ /// Tests the specified versions for equality.
+ ///
+ /// The first version.
+ /// The second version.
+ /// If versionA is equal to versionB true, else false.
+ public static bool Equals(SemVersion versionA, SemVersion versionB)
+ {
+ if (ReferenceEquals(versionA, null))
+ return ReferenceEquals(versionB, null);
+ return versionA.Equals(versionB);
+ }
+
+ ///
+ /// Compares the specified versions.
+ ///
+ /// The version to compare to.
+ /// The version to compare against.
+ /// If versionA < versionB < 0, if versionA > versionB > 0,
+ /// if versionA is equal to versionB 0.
+ public static int Compare(SemVersion versionA, SemVersion versionB)
+ {
+ if (ReferenceEquals(versionA, null))
+ return ReferenceEquals(versionB, null) ? 0 : -1;
+ return versionA.CompareTo(versionB);
+ }
+
+ ///
+ /// Make a copy of the current instance with optional altered fields.
+ ///
+ /// The major version.
+ /// The minor version.
+ /// The patch version.
+ /// The prerelease text.
+ /// The build text.
+ /// The new version object.
+ public SemVersion Change(int? major = null, int? minor = null, int? patch = null,
+ string prerelease = null, string build = null)
+ {
+ return new SemVersion(
+ major ?? this.Major,
+ minor ?? this.Minor,
+ patch ?? this.Patch,
+ prerelease ?? this.Prerelease,
+ build ?? this.Build);
+ }
+
+ ///
+ /// Gets the major version.
+ ///
+ ///
+ /// The major version.
+ ///
+ public int Major { get; private set; }
+
+ ///
+ /// Gets the minor version.
+ ///
+ ///
+ /// The minor version.
+ ///
+ public int Minor { get; private set; }
+
+ ///
+ /// Gets the patch version.
+ ///
+ ///
+ /// The patch version.
+ ///
+ public int Patch { get; private set; }
+
+ ///
+ /// Gets the pre-release version.
+ ///
+ ///
+ /// The pre-release version.
+ ///
+ public string Prerelease { get; private set; }
+
+ ///
+ /// Gets the build version.
+ ///
+ ///
+ /// The build version.
+ ///
+ public string Build { get; private set; }
+
+ ///
+ /// Returns a that represents this instance.
+ ///
+ ///
+ /// A that represents this instance.
+ ///
+ public override string ToString()
+ {
+ var version = "" + Major + "." + Minor + "." + Patch;
+ if (!String.IsNullOrEmpty(Prerelease))
+ version += "-" + Prerelease;
+ if (!String.IsNullOrEmpty(Build))
+ version += "+" + Build;
+ return version;
+ }
+
+ ///
+ /// Compares the current instance with another object of the same type and returns an integer that indicates
+ /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
+ /// other object.
+ ///
+ /// An object to compare with this instance.
+ ///
+ /// A value that indicates the relative order of the objects being compared.
+ /// The return value has these meanings: Value Meaning Less than zero
+ /// This instance precedes in the sort order.
+ /// Zero This instance occurs in the same position in the sort order as . i
+ /// Greater than zero This instance follows in the sort order.
+ ///
+ public int CompareTo(object obj)
+ {
+ return CompareTo((SemVersion)obj);
+ }
+
+ ///
+ /// Compares the current instance with another object of the same type and returns an integer that indicates
+ /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
+ /// other object.
+ ///
+ /// An object to compare with this instance.
+ ///
+ /// A value that indicates the relative order of the objects being compared.
+ /// The return value has these meanings: Value Meaning Less than zero
+ /// This instance precedes in the sort order.
+ /// Zero This instance occurs in the same position in the sort order as . i
+ /// Greater than zero This instance follows in the sort order.
+ ///
+ public int CompareTo(SemVersion other)
+ {
+ if (ReferenceEquals(other, null))
+ return 1;
+
+ var r = this.CompareByPrecedence(other);
+ if (r != 0)
+ return r;
+
+ r = CompareComponent(this.Build, other.Build);
+ return r;
+ }
+
+ ///
+ /// Compares to semantic versions by precedence. This does the same as a Equals, but ignores the build information.
+ ///
+ /// The semantic version.
+ /// true if the version precedence matches.
+ public bool PrecedenceMatches(SemVersion other)
+ {
+ return CompareByPrecedence(other) == 0;
+ }
+
+ ///
+ /// Compares to semantic versions by precedence. This does the same as a Equals, but ignores the build information.
+ ///
+ /// The semantic version.
+ ///
+ /// A value that indicates the relative order of the objects being compared.
+ /// The return value has these meanings: Value Meaning Less than zero
+ /// This instance precedes in the version precedence.
+ /// Zero This instance has the same precedence as . i
+ /// Greater than zero This instance has creater precedence as .
+ ///
+ public int CompareByPrecedence(SemVersion other)
+ {
+ if (ReferenceEquals(other, null))
+ return 1;
+
+ var r = this.Major.CompareTo(other.Major);
+ if (r != 0) return r;
+
+ r = this.Minor.CompareTo(other.Minor);
+ if (r != 0) return r;
+
+ r = this.Patch.CompareTo(other.Patch);
+ if (r != 0) return r;
+
+ r = CompareComponent(this.Prerelease, other.Prerelease, true);
+ return r;
+ }
+
+ static int CompareComponent(string a, string b, bool lower = false)
+ {
+ var aEmpty = String.IsNullOrEmpty(a);
+ var bEmpty = String.IsNullOrEmpty(b);
+ if (aEmpty && bEmpty)
+ return 0;
+
+ if (aEmpty)
+ return lower ? 1 : -1;
+ if (bEmpty)
+ return lower ? -1 : 1;
+
+ var aComps = a.Split('.');
+ var bComps = b.Split('.');
+
+ var minLen = Math.Min(aComps.Length, bComps.Length);
+ for (int i = 0; i < minLen; i++)
+ {
+ var ac = aComps[i];
+ var bc = bComps[i];
+ int anum, bnum;
+ var isanum = Int32.TryParse(ac, out anum);
+ var isbnum = Int32.TryParse(bc, out bnum);
+ int r;
+ if (isanum && isbnum)
+ {
+ r = anum.CompareTo(bnum);
+ if (r != 0) return anum.CompareTo(bnum);
+ }
+ else
+ {
+ if (isanum)
+ return -1;
+ if (isbnum)
+ return 1;
+ r = String.CompareOrdinal(ac, bc);
+ if (r != 0)
+ return r;
+ }
+ }
+
+ return aComps.Length.CompareTo(bComps.Length);
+ }
+
+ ///
+ /// Determines whether the specified is equal to this instance.
+ ///
+ /// The to compare with this instance.
+ ///
+ /// true if the specified is equal to this instance; otherwise, false.
+ ///
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(obj, null))
+ return false;
+
+ if (ReferenceEquals(this, obj))
+ return true;
+
+ var other = (SemVersion)obj;
+
+ return this.Major == other.Major &&
+ this.Minor == other.Minor &&
+ this.Patch == other.Patch &&
+ string.Equals(this.Prerelease, other.Prerelease, StringComparison.Ordinal) &&
+ string.Equals(this.Build, other.Build, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Returns a hash code for this instance.
+ ///
+ ///
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int result = this.Major.GetHashCode();
+ result = result * 31 + this.Minor.GetHashCode();
+ result = result * 31 + this.Patch.GetHashCode();
+ result = result * 31 + this.Prerelease.GetHashCode();
+ result = result * 31 + this.Build.GetHashCode();
+ return result;
+ }
+ }
+
+#if !NETSTANDARD
+ [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
+ public void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ if (info == null) throw new ArgumentNullException("info");
+ info.AddValue("SemVersion", ToString());
+ }
+#endif
+
+ ///
+ /// The override of the equals operator.
+ ///
+ /// The left value.
+ /// The right value.
+ /// If left is equal to right true, else false.
+ public static bool operator ==(SemVersion left, SemVersion right)
+ {
+ if(ReferenceEquals(right, null)) {
+ return ReferenceEquals(left, null);
+ }
+ if(ReferenceEquals(left, null)) {
+ return false;
+ }
+ return left.PrecedenceMatches(right);
+ }
+
+ ///
+ /// The override of the un-equal operator.
+ ///
+ /// The left value.
+ /// The right value.
+ /// If left is not equal to right true, else false.
+ public static bool operator !=(SemVersion left, SemVersion right)
+ {
+ return !(left == right);
+ }
+
+ ///
+ /// The override of the greater operator.
+ ///
+ /// The left value.
+ /// The right value.
+ /// If left is greater than right true, else false.
+ public static bool operator >(SemVersion left, SemVersion right)
+ {
+ return left.CompareByPrecedence(right) > 0;
+ }
+
+ ///
+ /// The override of the greater than or equal operator.
+ ///
+ /// The left value.
+ /// The right value.
+ /// If left is greater than or equal to right true, else false.
+ public static bool operator >=(SemVersion left, SemVersion right)
+ {
+ return left == right || left > right;
+ }
+
+ ///
+ /// The override of the less operator.
+ ///
+ /// The left value.
+ /// The right value.
+ /// If left is less than right true, else false.
+ public static bool operator <(SemVersion left, SemVersion right)
+ {
+ return left.CompareByPrecedence(right) < 0;
+ }
+
+ ///
+ /// The override of the less than or equal operator.
+ ///
+ /// The left value.
+ /// The right value.
+ /// If left is less than or equal to right true, else false.
+ public static bool operator <=(SemVersion left, SemVersion right)
+ {
+ return left == right || left < right;
+ }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Versioning/SemVer.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Versioning/SemVer.cs.meta
new file mode 100644
index 0000000..cc7e5d9
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Versioning/SemVer.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: 0b49a1188451e7745af9f636d854efc8
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Versioning/SemVer.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window.meta b/Packages/com.singularitygroup.hotreload/Editor/Window.meta
new file mode 100644
index 0000000..710dd15
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e1826b88dea6aa446a9bc22bc0140c22
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI.meta
new file mode 100644
index 0000000..5cbd648
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: dddc1cae3f951f84da98305ec6228f25
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons.meta
new file mode 100644
index 0000000..4d51a80
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 86f1446dfdbc2a94aac993437231aaa4
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenDialogueButton.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenDialogueButton.cs
new file mode 100644
index 0000000..3ecfc20
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenDialogueButton.cs
@@ -0,0 +1,42 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+ internal class OpenDialogueButton : IGUIComponent {
+ public readonly string text;
+ public readonly string url;
+ public readonly string title;
+ public readonly string message;
+ public readonly string ok;
+ public readonly string cancel;
+
+ public OpenDialogueButton(string text, string url, string title, string message, string ok, string cancel) {
+ this.text = text;
+ this.url = url;
+ this.title = title;
+ this.message = message;
+ this.ok = ok;
+ this.cancel = cancel;
+ }
+
+ public void OnGUI() {
+ Render(text, url, title, message, ok, cancel);
+ }
+
+ public static void Render(string text, string url, string title, string message, string ok, string cancel) {
+ if (GUILayout.Button(new GUIContent(text.StartsWith(" ") ? text : " " + text))) {
+ if (EditorUtility.DisplayDialog(title, message, ok, cancel)) {
+ Application.OpenURL(url);
+ }
+ }
+ }
+
+ public static void RenderRaw(Rect rect, string text, string url, string title, string message, string ok, string cancel, GUIStyle style = null) {
+ if (GUI.Button(rect, new GUIContent(text.StartsWith(" ") ? text : " " + text), style ?? GUI.skin.button)) {
+ if (EditorUtility.DisplayDialog(title, message, ok, cancel)) {
+ Application.OpenURL(url);
+ }
+ }
+ }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenDialogueButton.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenDialogueButton.cs.meta
new file mode 100644
index 0000000..6018d35
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenDialogueButton.cs.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 97ca8174f0514e8e9ee5d4be26ed8078
+timeCreated: 1674416481
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenDialogueButton.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenURLButton.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenURLButton.cs
new file mode 100644
index 0000000..0f1edcc
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenURLButton.cs
@@ -0,0 +1,29 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+ internal class OpenURLButton : IGUIComponent {
+ public readonly string text;
+ public readonly string url;
+ public OpenURLButton(string text, string url) {
+ this.text = text;
+ this.url = url;
+ }
+
+ public void OnGUI() {
+ Render(text, url);
+ }
+
+ public static void Render(string text, string url) {
+ if (GUILayout.Button(new GUIContent(text.StartsWith(" ") ? text : " " + text))) {
+ Application.OpenURL(url);
+ }
+ }
+
+ public static void RenderRaw(Rect rect, string text, string url, GUIStyle style = null) {
+ if (GUI.Button(rect, new GUIContent(text.StartsWith(" ") ? text : " " + text), style ?? GUI.skin.button)) {
+ Application.OpenURL(url);
+ }
+ }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenURLButton.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenURLButton.cs.meta
new file mode 100644
index 0000000..1150305
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenURLButton.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: ef12252fc9d1f9f438cbd34cf8f7364b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Buttons/OpenURLButton.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/EditorTextures.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/EditorTextures.cs
new file mode 100644
index 0000000..d20fae9
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/EditorTextures.cs
@@ -0,0 +1,116 @@
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+ ///
+ /// Create a new texture only once. Safe access to generated textures.
+ ///
+ ///
+ /// If
+ internal static class EditorTextures {
+ private static Texture2D black;
+ private static Texture2D white;
+ private static Texture2D lightGray225;
+ private static Texture2D lightGray235;
+ private static Texture2D darkGray17;
+ private static Texture2D darkGray30;
+
+ // Texture2D.blackTexture doesn't render properly in Editor GUI.
+ public static Texture2D Black {
+ get {
+ if (!black) {
+ black = new Texture2D(2, 2, TextureFormat.RGBA32, false);
+
+ var pixels = black.GetPixels32();
+ for (var i = 0; i < pixels.Length; i++) {
+ pixels[i] = new Color32(0, 0, 0, byte.MaxValue);
+ }
+ black.SetPixels32(pixels);
+ black.Apply();
+ }
+ return black;
+ }
+ }
+
+ // Texture2D.whiteTexture might not render properly in Editor GUI.
+ public static Texture2D White {
+ get {
+
+ if (!white) {
+ white = new Texture2D(2, 2, TextureFormat.RGBA32, false);
+
+ var pixels = white.GetPixels32();
+ for (var i = 0; i < pixels.Length; i++) {
+ pixels[i] = new Color32(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
+ }
+ white.SetPixels32(pixels);
+ white.Apply();
+ }
+ return white;
+ }
+ }
+
+ public static Texture2D DarkGray17 {
+ get {
+ if (!darkGray17) {
+ darkGray17 = new Texture2D(2, 2, TextureFormat.RGBA32, false);
+
+ var pixels = darkGray17.GetPixels32();
+ for (var i = 0; i < pixels.Length; i++) {
+ pixels[i] = new Color32(17, 17, 17, byte.MaxValue);
+ }
+ darkGray17.SetPixels32(pixels);
+ darkGray17.Apply();
+ }
+ return darkGray17;
+ }
+ }
+
+ public static Texture2D DarkGray40 {
+ get {
+ if (!darkGray30) {
+ darkGray30 = new Texture2D(2, 2, TextureFormat.RGBA32, false);
+
+ var pixels = darkGray30.GetPixels32();
+ for (var i = 0; i < pixels.Length; i++) {
+ pixels[i] = new Color32(40, 40, 40, byte.MaxValue);
+ }
+ darkGray30.SetPixels32(pixels);
+ darkGray30.Apply();
+ }
+ return darkGray30;
+ }
+ }
+
+ public static Texture2D LightGray238 {
+ get {
+ if (!lightGray235) {
+ lightGray235 = new Texture2D(2, 2, TextureFormat.RGBA32, false);
+
+ var pixels = lightGray235.GetPixels32();
+ for (var i = 0; i < pixels.Length; i++) {
+ pixels[i] = new Color32(238, 238, 238, byte.MaxValue);
+ }
+ lightGray235.SetPixels32(pixels);
+ lightGray235.Apply();
+ }
+ return lightGray235;
+ }
+ }
+
+ public static Texture2D LightGray225 {
+ get {
+ if (!lightGray225) {
+ lightGray225 = new Texture2D(2, 2, TextureFormat.RGBA32, false);
+
+ var pixels = lightGray225.GetPixels32();
+ for (var i = 0; i < pixels.Length; i++) {
+ pixels[i] = new Color32(225, 225, 225, byte.MaxValue);
+ }
+ lightGray225.SetPixels32(pixels);
+ lightGray225.Apply();
+ }
+ return lightGray225;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/EditorTextures.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/EditorTextures.cs.meta
new file mode 100644
index 0000000..f96d000
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/EditorTextures.cs.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 9116854180be4f2b8fcc0422bcf570a5
+timeCreated: 1674127121
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/EditorTextures.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/IGUIComponent.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/IGUIComponent.cs
new file mode 100644
index 0000000..323ce59
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/IGUIComponent.cs
@@ -0,0 +1,5 @@
+namespace SingularityGroup.HotReload.Editor {
+ internal interface IGUIComponent {
+ void OnGUI();
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/IGUIComponent.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/IGUIComponent.cs.meta
new file mode 100644
index 0000000..705aef6
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/IGUIComponent.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: 893cb208871dab94488cb988920f0ebd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/IGUIComponent.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options.meta
new file mode 100644
index 0000000..32dff3d
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2b3fa5ea1ed3545429de96b41801942f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/AllowAndroidAppToMakeHttpRequestsOption.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/AllowAndroidAppToMakeHttpRequestsOption.cs
new file mode 100644
index 0000000..1374b2c
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/AllowAndroidAppToMakeHttpRequestsOption.cs
@@ -0,0 +1,49 @@
+using SingularityGroup.HotReload.Editor.Localization;
+using UnityEditor;
+
+namespace SingularityGroup.HotReload.Editor {
+ internal class AllowAndroidAppToMakeHttpRequestsOption : ProjectOptionBase {
+ public override string ShortSummary {
+ get {
+ return Translations.Settings.OptionAllowHttpRequests;
+ }
+ }
+
+ public override string Summary => ShortSummary;
+
+ public override bool GetValue(SerializedObject so) {
+ #if UNITY_2022_1_OR_NEWER
+ // use PlayerSettings as the source of truth
+ return PlayerSettings.insecureHttpOption != InsecureHttpOption.NotAllowed;
+ #else
+ return GetProperty(so).boolValue;
+ #endif
+ }
+
+ public override string ObjectPropertyName =>
+ nameof(HotReloadSettingsObject.AllowAndroidAppToMakeHttpRequests);
+
+ public override void SetValue(SerializedObject so, bool value) {
+ base.SetValue(so, value);
+
+ // Enabling on Unity 2022 or newer → set the Unity option to ‘Development Builds only’
+ #if UNITY_2022_1_OR_NEWER
+ var notAllowed = PlayerSettings.insecureHttpOption == InsecureHttpOption.NotAllowed;
+ if (value) {
+ // user chose to enable it
+ if (notAllowed) {
+ PlayerSettings.insecureHttpOption = InsecureHttpOption.DevelopmentOnly;
+ }
+ } else {
+ // user chose to disable it
+ PlayerSettings.insecureHttpOption = InsecureHttpOption.NotAllowed;
+ }
+ #endif
+ }
+
+ public override void InnerOnGUI(SerializedObject so) {
+ var description = Translations.Settings.OptionAllowHttpRequestsDescription;
+ EditorGUILayout.LabelField(description, HotReloadWindowStyles.WrapStyle);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/AllowAndroidAppToMakeHttpRequestsOption.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/AllowAndroidAppToMakeHttpRequestsOption.cs.meta
new file mode 100644
index 0000000..386aa56
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/AllowAndroidAppToMakeHttpRequestsOption.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: 0a7442cee510ab4498ca2a846e0c4e92
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/AllowAndroidAppToMakeHttpRequestsOption.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base.meta
new file mode 100644
index 0000000..a16bb70
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: bb8474c37f13d704d96b43e0f681680d
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/HotReloadOptionBase.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/HotReloadOptionBase.cs
new file mode 100644
index 0000000..8cfa457
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/HotReloadOptionBase.cs
@@ -0,0 +1,57 @@
+using UnityEditor;
+
+namespace SingularityGroup.HotReload.Editor {
+ ///
+ /// An option stored inside the current Unity project.
+ ///
+ internal abstract class ProjectOptionBase : IOption, ISerializedProjectOption {
+ public abstract string ShortSummary { get; }
+ public abstract string Summary { get; }
+
+ public virtual bool GetValue(SerializedObject so) {
+ return so.FindProperty(ObjectPropertyName).boolValue;
+ }
+
+ protected SerializedProperty GetProperty(SerializedObject so) {
+ return so.FindProperty(ObjectPropertyName);
+ }
+
+ public virtual void SetValue(SerializedObject so, bool value) {
+ so.FindProperty(ObjectPropertyName).boolValue = value;
+ }
+
+ public virtual void InnerOnGUI(SerializedObject so) { }
+
+ public abstract string ObjectPropertyName { get; }
+
+ ///
+ /// Override this if your option is not needed for on-device Hot Reload to work.
+ /// (by default, a project option must be true for Hot Reload to work)
+ ///
+ public virtual bool IsRequiredForBuild() {
+ return true;
+ }
+ }
+
+ ///
+ /// An option that is stored on the user's computer (shared between Unity projects).
+ ///
+ internal abstract class ComputerOptionBase : IOption {
+ public abstract string ShortSummary { get; }
+ public abstract string Summary { get; }
+
+ public abstract bool GetValue();
+
+ /// Uses for storing the value on the user's computer.
+ public virtual void SetValue(bool value) { }
+
+ public bool GetValue(SerializedObject so) => GetValue();
+
+ public virtual void SetValue(SerializedObject so, bool value) => SetValue(value);
+
+ void IOption.InnerOnGUI(SerializedObject so) {
+ InnerOnGUI();
+ }
+ public virtual void InnerOnGUI() { }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/HotReloadOptionBase.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/HotReloadOptionBase.cs.meta
new file mode 100644
index 0000000..c3e72b4
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/HotReloadOptionBase.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: dab8ef53c2ee30a40ab6a7e4abd1260c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/HotReloadOptionBase.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/OptionInterfaces.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/OptionInterfaces.cs
new file mode 100644
index 0000000..e68f3a1
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/OptionInterfaces.cs
@@ -0,0 +1,34 @@
+using UnityEditor;
+
+namespace SingularityGroup.HotReload.Editor {
+ public interface IOption {
+ string ShortSummary { get; }
+ string Summary { get; }
+
+ /// The wrapped by SerializedObject
+ bool GetValue(SerializedObject so);
+
+ ///
+ /// Handle the new value.
+ ///
+ ///
+ /// Note: caller must skip calling this if value same as GetValue!
+ ///
+ /// The wrapped by SerializedObject
+ ///
+ void SetValue(SerializedObject so, bool value);
+
+ /// The wrapped by SerializedObject
+ void InnerOnGUI(SerializedObject so);
+ }
+
+ ///
+ /// An option scoped to the current Unity project.
+ ///
+ ///
+ /// These options are intended to be shared with collaborators and used by Unity Player builds.
+ ///
+ public interface ISerializedProjectOption {
+ string ObjectPropertyName { get; }
+ }
+}
\ No newline at end of file
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/OptionInterfaces.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/OptionInterfaces.cs.meta
new file mode 100644
index 0000000..236488e
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/OptionInterfaces.cs.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 0a626aa97160471f85de4646a634bdf1
+timeCreated: 1674574633
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/Base/OptionInterfaces.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/ExposeServerOption.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/ExposeServerOption.cs
new file mode 100644
index 0000000..732946b
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/ExposeServerOption.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Threading.Tasks;
+using SingularityGroup.HotReload.Editor.Cli;
+using SingularityGroup.HotReload.Editor.Localization;
+using UnityEditor;
+
+namespace SingularityGroup.HotReload.Editor {
+ internal sealed class ExposeServerOption : ComputerOptionBase {
+
+ public override string ShortSummary => Translations.Settings.OptionExposeServerShort;
+ public override string Summary => Translations.Settings.OptionExposeServerFull;
+
+ public override void InnerOnGUI() {
+ string description;
+ if (GetValue()) {
+ description = Translations.Settings.OptionExposeServerDescriptionEnabled;
+ } else {
+ description = Translations.Settings.OptionExposeServerDescriptionDisabled;
+ }
+ EditorGUILayout.LabelField(description, HotReloadWindowStyles.WrapStyle);
+ }
+
+ public override bool GetValue() {
+ return HotReloadPrefs.ExposeServerToLocalNetwork;
+ }
+
+ public override void SetValue(SerializedObject so, bool val) {
+ // AllowAndroidAppToMakeHttpRequestsOption
+ if (val == HotReloadPrefs.ExposeServerToLocalNetwork) {
+ return;
+ }
+
+ HotReloadPrefs.ExposeServerToLocalNetwork = val;
+ if (val) {
+ // they allowed this one for mobile builds, so now we allow everything else needed for player build to work with HR
+ new AllowAndroidAppToMakeHttpRequestsOption().SetValue(so, true);
+ }
+ RunTask(() => {
+ RunOnMainThreadSync(() => {
+ var isRunningResult = ServerHealthCheck.I.IsServerHealthy;
+ if (isRunningResult) {
+ var restartServer = EditorUtility.DisplayDialog(Translations.Dialogs.DialogTitleHotReload,
+ string.Format(Translations.Dialogs.DialogMessageRestartExposeServer, Summary),
+ Translations.Dialogs.DialogButtonRestartServer, Translations.Dialogs.DialogButtonDontRestart);
+ if (restartServer) {
+ CodePatcher.I.ClearPatchedMethods();
+ EditorCodePatcher.RestartCodePatcher().Forget();
+ }
+ }
+ });
+ });
+ }
+
+ void RunTask(Action action) {
+ var token = HotReloadWindow.Current.cancelToken;
+ Task.Run(() => {
+ if (token.IsCancellationRequested) return;
+ try {
+ action();
+ } catch (Exception ex) {
+ ThreadUtility.LogException(ex, token);
+ }
+ }, token);
+ }
+
+ void RunOnMainThreadSync(Action action) {
+ ThreadUtility.RunOnMainThread(action, HotReloadWindow.Current.cancelToken);
+ }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/ExposeServerOption.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/ExposeServerOption.cs.meta
new file mode 100644
index 0000000..aea65a1
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/ExposeServerOption.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: 5ab0973d3ae1275469237480381842c0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/ExposeServerOption.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/IncludeInBuildOption.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/IncludeInBuildOption.cs
new file mode 100644
index 0000000..320faf9
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/IncludeInBuildOption.cs
@@ -0,0 +1,25 @@
+using SingularityGroup.HotReload.Editor.Localization;
+using UnityEditor;
+
+namespace SingularityGroup.HotReload.Editor {
+ internal class IncludeInBuildOption : ProjectOptionBase, ISerializedProjectOption {
+ static IncludeInBuildOption _I;
+ public static IncludeInBuildOption I = _I ?? (_I = new IncludeInBuildOption());
+ public override string ShortSummary => Translations.Settings.OptionIncludeInBuild;
+ public override string Summary => ShortSummary;
+
+ public override string ObjectPropertyName =>
+ nameof(HotReloadSettingsObject.IncludeInBuild);
+
+ public override void InnerOnGUI(SerializedObject so) {
+ string description;
+ if (GetValue(so)) {
+ description = Translations.Settings.OptionIncludeInBuildDescriptionEnabled;
+ } else {
+ description = Translations.Settings.OptionIncludeInBuildDescriptionDisabled;
+ }
+ description += Translations.Settings.OptionIncludeInBuildDescriptionSuffix;
+ EditorGUILayout.LabelField(description, HotReloadWindowStyles.WrapStyle);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/IncludeInBuildOption.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/IncludeInBuildOption.cs.meta
new file mode 100644
index 0000000..2ab1317
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/IncludeInBuildOption.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: 39ed4f822bcd81340bdf7189b3bc5016
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Options/IncludeInBuildOption.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs.meta
new file mode 100644
index 0000000..b777231
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9c0f7811020465d46bcd0305e2f83e8a
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Base.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Base.meta
new file mode 100644
index 0000000..9cec40c
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Base.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 58d14712b7ef14540ba4817a5ef873a6
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Base/HotReloadTabBase.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Base/HotReloadTabBase.cs
new file mode 100644
index 0000000..7a64888
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Base/HotReloadTabBase.cs
@@ -0,0 +1,33 @@
+
+using UnityEditor;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+ internal abstract class HotReloadTabBase : IGUIComponent {
+ protected readonly HotReloadWindow _window;
+
+ public string Title { get; }
+ public Texture Icon { get; }
+ public string Tooltip { get; }
+
+ public HotReloadTabBase(HotReloadWindow window, string title, Texture iconImage, string tooltip) {
+ _window = window;
+
+ Title = title;
+ Icon = iconImage;
+ Tooltip = tooltip;
+ }
+
+ public HotReloadTabBase(HotReloadWindow window, string title, string iconName, string tooltip) :
+ this(window, title, EditorGUIUtility.IconContent(iconName).image, tooltip) {
+ }
+
+ protected void Repaint() {
+ _window.Repaint();
+ }
+
+ public virtual void Update() { }
+
+ public abstract void OnGUI();
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Base/HotReloadTabBase.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Base/HotReloadTabBase.cs.meta
new file mode 100644
index 0000000..37db52d
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Base/HotReloadTabBase.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: c2c79b82bd9636d499449f91f93fae2a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Base/HotReloadTabBase.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Helpers.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Helpers.meta
new file mode 100644
index 0000000..0c5c6ba
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Helpers.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: a089a7225d904b00b2893a34b514ad28
+timeCreated: 1689791626
\ No newline at end of file
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Helpers/RedeemLicenseHelper.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Helpers/RedeemLicenseHelper.cs
new file mode 100644
index 0000000..b282964
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Helpers/RedeemLicenseHelper.cs
@@ -0,0 +1,308 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using SingularityGroup.HotReload.DTO;
+using SingularityGroup.HotReload.Editor.Localization;
+using SingularityGroup.HotReload.Newtonsoft.Json;
+using UnityEditor;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload.Editor {
+ internal enum RedeemStage {
+ None,
+ Registration,
+ Redeem,
+ Login
+ }
+
+ // IMPORTANT: don't rename
+ internal enum RegistrationOutcome {
+ None,
+ Indie,
+ Business,
+ }
+
+ internal class RedeemLicenseHelper {
+ public static readonly RedeemLicenseHelper I = new RedeemLicenseHelper();
+
+ private string _pendingCompanySize;
+ private string _pendingInvoiceNumber;
+ private string _pendingRedeemEmail;
+
+ private static string registerFlagPath = PackageConst.LibraryCachePath + "/registerFlag.txt";
+ public static string registerOutcomePath = PackageConst.LibraryCachePath + "/registerOutcome.txt";
+
+ public RedeemStage RedeemStage { get; private set; }
+ public RegistrationOutcome RegistrationOutcome { get; private set; }
+ public bool RegistrationRequired => RedeemStage != RedeemStage.None;
+
+ private string status;
+ private string error;
+
+ const string statusSuccess = "success";
+ const string statusAlreadyClaimed = "already redeemed by this user/device";
+
+ private GUILayoutOption[] secondaryButtonLayoutOptions = new[] { GUILayout.MaxWidth(100) };
+
+ private bool requestingRedeem;
+ private HttpClient redeemClient;
+ const string redeemUrl = "https://vmhzj6jonn3qy7hk7tx7levpli0bstpj.lambda-url.us-east-1.on.aws/redeem";
+
+ public RedeemLicenseHelper() {
+ if (File.Exists(registerFlagPath)) {
+ RedeemStage = RedeemStage.Registration;
+ }
+ try {
+ if (File.Exists(registerOutcomePath)) {
+ RegistrationOutcome outcome;
+ if (Enum.TryParse(File.ReadAllText(registerOutcomePath), out outcome)) {
+ RegistrationOutcome = outcome;
+ }
+ }
+ } catch (Exception e) {
+ Log.Warning(Translations.Errors.WarningFailedDeterminingRegistration, e.GetType().Name, e.Message);
+ }
+ }
+
+ public void RenderStage(HotReloadRunTabState state) {
+ if (state.redeemStage == RedeemStage.Registration) {
+ RenderRegistration();
+ } else if (state.redeemStage == RedeemStage.Redeem) {
+ RenderRedeem();
+ } else if (state.redeemStage == RedeemStage.Login) {
+ RenderLogin(state);
+ }
+ }
+
+ private void RenderRegistration() {
+ var message = PackageConst.IsAssetStoreBuild
+ ? Translations.Registration.MessageRegistrationProUsers
+ : Translations.Registration.MessageRegistrationLicensingModel;
+ if (error != null) {
+ EditorGUILayout.HelpBox(error, MessageType.Warning);
+ } else {
+ EditorGUILayout.HelpBox(message, MessageType.Info);
+ }
+ EditorGUILayout.Space();
+ EditorGUILayout.Space();
+
+ EditorGUILayout.LabelField(Translations.Common.LabelCompanySize);
+ GUI.SetNextControlName("company_size");
+ _pendingCompanySize = EditorGUILayout.TextField(_pendingCompanySize)?.Trim();
+ EditorGUILayout.Space();
+
+ if (GUILayout.Button(Translations.Common.ButtonProceed)) {
+ int companySize;
+ if (!int.TryParse(_pendingCompanySize, out companySize)) {
+ error = Translations.Errors.ErrorEnterNumber;
+ } else {
+ error = null;
+ HandleRegistration(companySize);
+ }
+ }
+ }
+
+ void HandleRegistration(int companySize) {
+ RequestHelper.RequestEditorEvent(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.Licensing, StatEventType.Register), new EditorExtraData { { StatKey.CompanySize, companySize } });
+ if (companySize > 10) {
+ FinishRegistration(RegistrationOutcome.Business);
+ EditorCodePatcher.DownloadAndRun().Forget();
+ } else if (PackageConst.IsAssetStoreBuild) {
+ SwitchToStage(RedeemStage.Redeem);
+ } else {
+ FinishRegistration(RegistrationOutcome.Indie);
+ EditorCodePatcher.DownloadAndRun().Forget();
+ }
+ }
+
+ private void RenderRedeem() {
+ if (error != null) {
+ EditorGUILayout.HelpBox(error, MessageType.Warning);
+ } else {
+ EditorGUILayout.HelpBox(Translations.Registration.MessageRedeemInstructions, MessageType.Info);
+ }
+ EditorGUILayout.Space();
+ EditorGUILayout.Space();
+
+ EditorGUILayout.LabelField(Translations.Common.LabelInvoiceNumber);
+ GUI.SetNextControlName("invoice_number");
+ _pendingInvoiceNumber = EditorGUILayout.TextField(_pendingInvoiceNumber ?? HotReloadPrefs.RedeemLicenseInvoice)?.Trim();
+ EditorGUILayout.Space();
+
+ EditorGUILayout.LabelField(Translations.Common.LabelEmail);
+ GUI.SetNextControlName("email_redeem");
+ _pendingRedeemEmail = EditorGUILayout.TextField(_pendingRedeemEmail ?? HotReloadPrefs.RedeemLicenseEmail);
+ EditorGUILayout.Space();
+
+ using (new EditorGUI.DisabledScope(requestingRedeem)) {
+ if (GUILayout.Button(Translations.Common.ButtonRedeem, HotReloadRunTab.bigButtonHeight)) {
+ RedeemLicense(email: _pendingRedeemEmail, invoiceNumber: _pendingInvoiceNumber).Forget();
+ }
+ }
+ EditorGUILayout.Space();
+ EditorGUILayout.Space();
+
+ using (new EditorGUILayout.HorizontalScope()) {
+ GUILayout.FlexibleSpace();
+ if (GUILayout.Button(Translations.Common.ButtonSkip, secondaryButtonLayoutOptions)) {
+ SwitchToStage(RedeemStage.Login);
+ }
+ GUILayout.FlexibleSpace();
+ }
+ }
+
+ async Task RedeemLicense(string email, string invoiceNumber) {
+ string validationError;
+ if (string.IsNullOrEmpty(invoiceNumber)) {
+ validationError = Translations.Errors.ErrorEnterInvoiceNumber;
+ } else {
+ validationError = HotReloadRunTab.ValidateEmail(email);
+ }
+ if (validationError != null) {
+ error = validationError;
+ return;
+ }
+ var resp = await RequestRedeem(email: email, invoiceNumber: invoiceNumber);
+ status = resp?.status;
+ if (status != null) {
+ if (status != statusSuccess && status != statusAlreadyClaimed) {
+ Log.Error(Translations.Errors.WarningRedeemStatusUnknown);
+ error = Translations.Registration.UnknownRedeemError;
+ } else {
+ HotReloadPrefs.RedeemLicenseEmail = email;
+ HotReloadPrefs.RedeemLicenseInvoice = invoiceNumber;
+ // prepare data for login screen
+ HotReloadPrefs.LicenseEmail = email;
+ HotReloadPrefs.LicensePassword = null;
+
+ SwitchToStage(RedeemStage.Login);
+ }
+ } else if (resp?.error != null) {
+ Log.Warning(Translations.Errors.WarningRedeemingLicenseFailed, resp.error);
+ error = GetPrettyError(resp);
+ } else {
+ Log.Warning(Translations.Errors.WarningRedeemUnknownError);
+ error = Translations.Registration.UnknownRedeemError;
+ }
+ }
+
+ string GetPrettyError(RedeemResponse response) {
+ var err = response?.error;
+ if (err == null) {
+ return Translations.Registration.UnknownRedeemError;
+ }
+ if (err.Contains("Invalid email")) {
+ return Translations.Errors.ErrorInvalidEmailAddress;
+ } else if (err.Contains("License invoice already redeemed")) {
+ return Translations.Errors.ErrorLicenseInvoiceRedeemed;
+ } else if (err.Contains("Different license already redeemed by given email")) {
+ return Translations.Errors.ErrorEmailAlreadyUsed;
+ } else if (err.Contains("Invoice not found")) {
+ return Translations.Errors.ErrorInvoiceNotFound;
+ } else if (err.Contains("Invoice refunded")) {
+ return Translations.Errors.ErrorInvoiceRefunded;
+ } else {
+ return Translations.Registration.UnknownRedeemError;
+ }
+ }
+
+ async Task RequestRedeem(string email, string invoiceNumber) {
+ requestingRedeem = true;
+ await ThreadUtility.SwitchToThreadPool();
+ try {
+ redeemClient = redeemClient ?? (redeemClient = HttpClientUtils.CreateHttpClient());
+ var input = new Dictionary {
+ { "email", email },
+ { "invoice", invoiceNumber }
+ };
+ var content = new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, "application/json");
+ using (var resp = await redeemClient.PostAsync(redeemUrl, content, HotReloadWindow.Current.cancelToken).ConfigureAwait(false)) {
+ if (resp.StatusCode != HttpStatusCode.OK) {
+ return new RedeemResponse(null, string.Format(Translations.Errors.ErrorRedeemRequestFailed, (int)resp.StatusCode, resp.ReasonPhrase));
+ }
+ var str = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
+ try {
+ return JsonConvert.DeserializeObject(str);
+ } catch (Exception ex) {
+ return new RedeemResponse(null, string.Format(Translations.Errors.ErrorFailedDeserializingRedeem, ex.GetType().Name, ex.Message));
+ }
+ }
+ } catch (WebException ex) {
+ return new RedeemResponse(null, string.Format(Translations.Errors.ErrorRedeemingWebException, ex.Message));
+ } finally {
+ requestingRedeem = false;
+ }
+ }
+
+ private class RedeemResponse {
+ public string status;
+ public string error;
+
+ public RedeemResponse(string status, string error) {
+ this.status = status;
+ this.error = error;
+ }
+ }
+
+ private void RenderLogin(HotReloadRunTabState state) {
+ if (status == statusSuccess) {
+ EditorGUILayout.HelpBox(Translations.Registration.MessageRedeemSuccess, MessageType.Info);
+ } else if (status == statusAlreadyClaimed) {
+ EditorGUILayout.HelpBox(Translations.Registration.MessageRedeemAlreadyClaimed, MessageType.Info);
+ }
+ EditorGUILayout.Space();
+ EditorGUILayout.Space();
+
+ HotReloadRunTab.RenderLicenseInnerPanel(state, renderLogout: false);
+ EditorGUILayout.Space();
+ EditorGUILayout.Space();
+
+ using (new EditorGUILayout.HorizontalScope()) {
+ GUILayout.FlexibleSpace();
+ if (GUILayout.Button(Translations.Common.ButtonGoBack, secondaryButtonLayoutOptions)) {
+ SwitchToStage(RedeemStage.Redeem);
+ }
+ GUILayout.FlexibleSpace();
+ }
+ }
+
+ public void StartRegistration() {
+ // ReSharper disable once AssignNullToNotNullAttribute
+ Directory.CreateDirectory(Path.GetDirectoryName(registerFlagPath));
+ using (File.Create(registerFlagPath)) {
+ }
+ RedeemStage = RedeemStage.Registration;
+ RegistrationOutcome = RegistrationOutcome.None;
+ }
+
+ public void FinishRegistration(RegistrationOutcome outcome) {
+ // ReSharper disable once AssignNullToNotNullAttribute
+ Directory.CreateDirectory(Path.GetDirectoryName(registerFlagPath));
+ File.WriteAllText(registerOutcomePath, outcome.ToString());
+ File.Delete(registerFlagPath);
+ RegistrationOutcome = outcome;
+ SwitchToStage(RedeemStage.None);
+ Cleanup();
+ }
+
+ void SwitchToStage(RedeemStage stage) {
+ // remove focus so that the input field re-renders
+ GUI.FocusControl(null);
+ RedeemStage = stage;
+ }
+
+ void Cleanup() {
+ redeemClient?.Dispose();
+ redeemClient = null;
+ _pendingCompanySize = null;
+ _pendingInvoiceNumber = null;
+ _pendingRedeemEmail = null;
+ status = null;
+ error = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Helpers/RedeemLicenseHelper.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Helpers/RedeemLicenseHelper.cs.meta
new file mode 100644
index 0000000..5386a59
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Helpers/RedeemLicenseHelper.cs.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: ad73f74d3c494c02aae937e2dfa305a2
+timeCreated: 1689791373
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/Helpers/RedeemLicenseHelper.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadAboutTab.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadAboutTab.cs
new file mode 100644
index 0000000..77b4a0c
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadAboutTab.cs
@@ -0,0 +1,312 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using SingularityGroup.HotReload.Editor.Localization;
+using UnityEditor;
+using UnityEngine;
+using System.Threading.Tasks;
+using System.IO;
+using SingularityGroup.HotReload.Newtonsoft.Json;
+using SingularityGroup.HotReload.EditorDependencies;
+
+namespace SingularityGroup.HotReload.Editor {
+ internal struct HotReloadAboutTabState {
+ public readonly bool logsFodlerExists;
+ public readonly IReadOnlyList changelog;
+ public readonly bool loginRequired;
+ public readonly bool hasTrialLicense;
+ public readonly bool hasPayedLicense;
+
+ public HotReloadAboutTabState(
+ bool logsFodlerExists,
+ IReadOnlyList changelog,
+ bool loginRequired,
+ bool hasTrialLicense,
+ bool hasPayedLicense
+ ) {
+ this.logsFodlerExists = logsFodlerExists;
+ this.changelog = changelog;
+ this.loginRequired = loginRequired;
+ this.hasTrialLicense = hasTrialLicense;
+ this.hasPayedLicense = hasPayedLicense;
+ }
+ }
+
+ internal class HotReloadAboutTab : HotReloadTabBase {
+ internal static readonly OpenURLButton seeMore = new OpenURLButton(Translations.About.ButtonSeeMore, Constants.ChangelogURL);
+ internal static readonly OpenDialogueButton manageLicenseButton = new OpenDialogueButton(Translations.About.ButtonManageLicense, Constants.ManageLicenseURL, Translations.About.ButtonManageLicense, Translations.Dialogs.DialogManageLicenseMessage, Translations.Common.ButtonOpenInBrowser, Translations.Common.ButtonCancel);
+ internal static readonly OpenDialogueButton manageAccountButton = new OpenDialogueButton(Translations.About.ButtonManageAccount, Constants.ManageAccountURL, Translations.About.ButtonManageAccount, Translations.Dialogs.DialogManageAccountMessage, Translations.Common.ButtonOpenInBrowser, Translations.Common.ButtonCancel);
+ internal static readonly OpenURLButton contactButton = new OpenURLButton(Translations.About.ButtonContact, Constants.ContactURL);
+ internal static readonly OpenURLButton discordButton = new OpenURLButton(Translations.About.ButtonJoinDiscord, Constants.DiscordInviteUrl);
+ internal static readonly OpenDialogueButton reportIssueButton = new OpenDialogueButton(Translations.About.ButtonReportIssue, Constants.ReportIssueURL, Translations.About.ButtonReportIssue, Translations.Dialogs.DialogReportIssueMessage, Translations.Common.ButtonOpenInBrowser, Translations.Common.ButtonCancel);
+
+ private Vector2 _changelogScroll;
+ private IReadOnlyList _changelog = new List();
+ private bool _requestedChangelog;
+ private int _changelogRequestAttempt;
+ private string _changelogDir = Path.Combine(PackageConst.LibraryCachePath, "changelog.json");
+ public static string logsPath = Path.Combine(PackageConst.LibraryCachePath, "logs");
+
+ private static bool LatestChangelogLoaded(IReadOnlyList changelog) {
+ return changelog.Any() && changelog[0].versionNum == PackageUpdateChecker.lastRemotePackageVersion;
+ }
+
+ private async Task FetchChangelog() {
+ if(!_changelog.Any()) {
+ var file = new FileInfo(_changelogDir);
+ if (file.Exists) {
+ await Task.Run(() => {
+ var bytes = File.ReadAllText(_changelogDir);
+ _changelog = JsonConvert.DeserializeObject>(bytes);
+ });
+ }
+ }
+ if (_requestedChangelog || LatestChangelogLoaded(_changelog)) {
+ return;
+ }
+ _requestedChangelog = true;
+ try {
+ do {
+ var changelogRequestTimeout = ExponentialBackoff.GetTimeout(_changelogRequestAttempt);
+ _changelog = await RequestHelper.FetchChangelog() ?? _changelog;
+ if (LatestChangelogLoaded(_changelog)) {
+ await Task.Run(() => {
+ Directory.CreateDirectory(PackageConst.LibraryCachePath);
+ File.WriteAllText(_changelogDir, JsonConvert.SerializeObject(_changelog));
+ });
+ Repaint();
+ return;
+ }
+ await Task.Delay(changelogRequestTimeout);
+ } while (_changelogRequestAttempt++ < 1000 && !LatestChangelogLoaded(_changelog));
+ } catch {
+ // ignore
+ } finally {
+ _requestedChangelog = false;
+ }
+ }
+
+ public HotReloadAboutTab(HotReloadWindow window) : base(window, Translations.About.AboutTitle, "_Help", Translations.About.AboutDescription) { }
+
+ string GetRelativeDate(DateTime givenDate) {
+ const int second = 1;
+ const int minute = 60 * second;
+ const int hour = 60 * minute;
+ const int day = 24 * hour;
+ const int month = 30 * day;
+
+ var ts = new TimeSpan(DateTime.UtcNow.Ticks - givenDate.Ticks);
+ var delta = Math.Abs(ts.TotalSeconds);
+
+ if (delta < 24 * hour)
+ return Translations.About.AboutToday;
+
+ if (delta < 48 * hour)
+ return Translations.About.AboutYesterday;
+
+ if (delta < 30 * day)
+ return string.Format(Translations.About.AboutDaysAgo, ts.Days);
+
+ if (delta < 12 * month) {
+ var months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
+ return months <= 1 ? Translations.About.AboutOneMonthAgo : string.Format(Translations.About.AboutMonthsAgo, months);
+ }
+ var years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
+ return years <= 1 ? Translations.About.AboutOneYearAgo : string.Format(Translations.About.AboutYearsAgo, years);
+ }
+
+ void RenderVersion(ChangelogVersion version) {
+ var tempTextString = "";
+
+ //version number
+ EditorGUILayout.TextArea(version.versionNum, HotReloadWindowStyles.H1TitleStyle);
+
+ //general info
+ if (version.generalInfo != null) {
+ EditorGUILayout.TextArea(version.generalInfo, HotReloadWindowStyles.H3TitleStyle);
+ }
+
+ //features
+ if (version.features != null) {
+ EditorGUILayout.TextArea(Translations.About.AboutFeatures, HotReloadWindowStyles.H2TitleStyle);
+ tempTextString = "";
+ foreach (var feature in version.features) {
+ tempTextString += "• " + feature + "\n";
+ }
+ EditorGUILayout.TextArea(tempTextString, HotReloadWindowStyles.ChangelogPointerStyle);
+ }
+
+ //improvements
+ if (version.improvements != null) {
+ EditorGUILayout.TextArea(Translations.About.AboutImprovements, HotReloadWindowStyles.H2TitleStyle);
+ tempTextString = "";
+ foreach (var improvement in version.improvements) {
+ tempTextString += "• " + improvement + "\n";
+ }
+ EditorGUILayout.TextArea(tempTextString, HotReloadWindowStyles.ChangelogPointerStyle);
+ }
+
+ //fixes
+ if (version.fixes != null) {
+ EditorGUILayout.TextArea(Translations.About.AboutFixes, HotReloadWindowStyles.H2TitleStyle);
+ tempTextString = "";
+ foreach (var fix in version.fixes) {
+ tempTextString += "• " + fix + "\n";
+ }
+ EditorGUILayout.TextArea(tempTextString, HotReloadWindowStyles.ChangelogPointerStyle);
+ }
+
+ //date
+ DateTime date;
+ if (DateTime.TryParseExact(version.date, "dd/MM/yyyy", null, DateTimeStyles.None, out date)) {
+ var relativeDate = GetRelativeDate(date);
+ GUILayout.TextArea(relativeDate, HotReloadWindowStyles.H3TitleStyle);
+ }
+ }
+
+ void RenderChangelog() {
+ FetchChangelog().Forget();
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionInnerBoxWide)) {
+ using (new EditorGUILayout.VerticalScope()) {
+ HotReloadPrefs.ShowChangeLog = EditorGUILayout.Foldout(HotReloadPrefs.ShowChangeLog, Translations.Miscellaneous.ChangelogTitle, true, HotReloadWindowStyles.FoldoutStyle);
+ if (!HotReloadPrefs.ShowChangeLog) {
+ return;
+ }
+ // changelog versions
+ var maxChangeLogs = 5;
+ var index = 0;
+ foreach (var version in currentState.changelog) {
+ index++;
+ if (index > maxChangeLogs) {
+ break;
+ }
+
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.ChangelogSectionInnerBox)) {
+ using (new EditorGUILayout.VerticalScope()) {
+ RenderVersion(version);
+ }
+ }
+ }
+ // see more button
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.ChangelogSectionInnerBox)) {
+ seeMore.OnGUI();
+ }
+ }
+ }
+ }
+
+ private Vector2 _aboutTabScrollPos;
+
+ HotReloadAboutTabState currentState;
+ public override void OnGUI() {
+ // HotReloadAboutTabState ensures rendering is consistent between Layout and Repaint calls
+ // Without it errors like this happen:
+ // ArgumentException: Getting control 2's position in a group with only 2 controls when doing repaint
+ // See thread for more context: https://answers.unity.com/questions/17718/argumentexception-getting-control-2s-position-in-a.html
+ if (Event.current.type == EventType.Layout) {
+ currentState = new HotReloadAboutTabState(
+ logsFodlerExists: Directory.Exists(logsPath),
+ changelog: _changelog,
+ loginRequired: EditorCodePatcher.LoginNotRequired,
+ hasTrialLicense: _window.RunTab.TrialLicense,
+ hasPayedLicense: _window.RunTab.HasPayedLicense
+ );
+ }
+ using (var scope = new EditorGUILayout.ScrollViewScope(_aboutTabScrollPos, GUI.skin.horizontalScrollbar, GUI.skin.verticalScrollbar, GUILayout.MaxHeight(Math.Max(HotReloadWindowStyles.windowScreenHeight, 800)), GUILayout.MaxWidth(Math.Max(HotReloadWindowStyles.windowScreenWidth, 800)))) {
+ _aboutTabScrollPos.x = scope.scrollPosition.x;
+ _aboutTabScrollPos.y = scope.scrollPosition.y;
+
+ using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.DynamicSectionHelpTab)) {
+ using (new EditorGUILayout.VerticalScope()) {
+ GUILayout.Space(10);
+ RenderLogButtons();
+
+ EditorGUILayout.Space();
+ EditorGUILayout.HelpBox(string.Format(Translations.About.AboutVersionInfo, PackageConst.Version), MessageType.Info);
+ EditorGUILayout.Space();
+
+ RenderHelpButtons();
+
+ GUILayout.Space(15);
+
+ try {
+ RenderChangelog();
+ } catch {
+ // ignore
+ }
+ }
+ }
+ }
+ }
+
+ void RenderHelpButtons() {
+ var labelRect = GUILayoutUtility.GetLastRect();
+ using (new EditorGUILayout.HorizontalScope()) {
+ using (new EditorGUILayout.VerticalScope()) {
+ var buttonHeight = 19;
+
+ var bigButtonRect = new Rect(labelRect.x + 3, labelRect.y + 5, labelRect.width - 6, buttonHeight);
+ OpenURLButton.RenderRaw(bigButtonRect, Translations.About.ButtonDocumentation, Constants.DocumentationURL, HotReloadWindowStyles.HelpTabButton);
+
+ var firstLayerX = bigButtonRect.x;
+ var firstLayerY = bigButtonRect.y + buttonHeight + 3;
+ var firstLayerWidth = (int)((bigButtonRect.width / 2) - 3);
+
+ var secondLayerX = firstLayerX + firstLayerWidth + 5;
+ var secondLayerY = firstLayerY + buttonHeight + 3;
+ var secondLayerWidth = bigButtonRect.width - firstLayerWidth - 5;
+
+ using (new EditorGUILayout.HorizontalScope()) {
+ OpenURLButton.RenderRaw(new Rect { x = firstLayerX, y = firstLayerY, width = firstLayerWidth, height = buttonHeight }, contactButton.text, contactButton.url, HotReloadWindowStyles.HelpTabButton);
+ OpenURLButton.RenderRaw(new Rect { x = secondLayerX, y = firstLayerY, width = secondLayerWidth, height = buttonHeight }, Translations.About.ButtonUnityForum, Constants.ForumURL, HotReloadWindowStyles.HelpTabButton);
+ }
+ using (new EditorGUILayout.HorizontalScope()) {
+ OpenDialogueButton.RenderRaw(rect: new Rect { x = firstLayerX, y = secondLayerY, width = firstLayerWidth, height = buttonHeight }, text: reportIssueButton.text, url: reportIssueButton.url, title: reportIssueButton.title, message: reportIssueButton.message, ok: reportIssueButton.ok, cancel: reportIssueButton.cancel, style: HotReloadWindowStyles.HelpTabButton);
+ OpenURLButton.RenderRaw(new Rect { x = secondLayerX, y = secondLayerY, width = secondLayerWidth, height = buttonHeight }, discordButton.text, discordButton.url, HotReloadWindowStyles.HelpTabButton);
+ }
+ }
+ }
+ GUILayout.Space(80);
+ }
+
+ void RenderLogButtons() {
+ if (currentState.logsFodlerExists) {
+ EditorGUILayout.Space();
+ EditorGUILayout.BeginHorizontal();
+ GUILayout.FlexibleSpace();
+ if (GUILayout.Button(Translations.Common.ButtonOpenLogFile)) {
+ var mostRecentFile = LogsHelper.FindRecentLog(logsPath);
+ if (mostRecentFile == null) {
+ Log.Info(Translations.About.LogNoLogsFound);
+ } else {
+ try {
+ Process.Start($"\"{Path.Combine(logsPath, mostRecentFile)}\"");
+ } catch (Win32Exception e) {
+ // TODO: is this the same for chinese?
+ if (e.Message.Contains("Application not found")) {
+ try {
+ Process.Start("notepad.exe", $"\"{Path.Combine(logsPath, mostRecentFile)}\"");
+ } catch {
+ // Fallback to opening folder with all logs
+ Process.Start($"\"{logsPath}\"");
+ Log.Info(Translations.About.LogFailedOpeningLogFile);
+ }
+ }
+ } catch {
+ // Fallback to opening folder with all logs
+ Process.Start($"\"{logsPath}\"");
+ Log.Info(Translations.About.LogFailedOpeningLogFile);
+ }
+ }
+ }
+ if (GUILayout.Button(Translations.Common.ButtonBrowseAllLogs)) {
+ Process.Start($"\"{logsPath}\"");
+ }
+ EditorGUILayout.EndHorizontal();
+ }
+ }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadAboutTab.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadAboutTab.cs.meta
new file mode 100644
index 0000000..3439e8d
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadAboutTab.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: 7cf8e9ef1ab770249a4318e88e882a85
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadAboutTab.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadOptionsSection.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadOptionsSection.cs
new file mode 100644
index 0000000..281c76f
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadOptionsSection.cs
@@ -0,0 +1,50 @@
+using UnityEditor;
+using UnityEngine;
+using SingularityGroup.HotReload.Editor.Localization;
+
+namespace SingularityGroup.HotReload.Editor {
+ internal class HotReloadOptionsSection {
+ ///
+ /// Opening options tab does not automatically create the settings asset file.
+ /// - The Options UI shows defaults if the object asset doesn't exist.
+ /// - When a build starts, we also ensure the asset file exists.
+ ///
+ public void DrawGUI(SerializedObject so) {
+ so.Update(); // must update in-case asset was modified externally
+
+ foreach (var option in HotReloadSettingsTab.allOptions) {
+ GUILayout.Space(4f);
+ DrawOption(option, so);
+ }
+
+ // commit any changes to the underlying ScriptableObject
+ if (so.hasModifiedProperties) {
+ so.ApplyModifiedProperties();
+ // Ensure asset file exists on disk, because we initially create it in memory (to provide the default values)
+ // This does not save the asset, user has to do that by saving assets in Unity (e.g. press hotkey Ctrl + S)
+ var target = so.targetObject as HotReloadSettingsObject;
+ if (target == null) {
+ Log.Warning(Translations.Errors.WarningUnexpectedSaveProblem);
+ } else {
+ // when one of the project options changed then we ensure the asset file exists.
+ HotReloadSettingsEditor.EnsureSettingsCreated(target);
+ }
+ }
+ }
+
+ static void DrawOption(IOption option, SerializedObject so) {
+ EditorGUILayout.BeginVertical(HotReloadWindowStyles.BoxStyle);
+
+ var before = option.GetValue(so);
+ var after = EditorGUILayout.BeginToggleGroup(new GUIContent(" " + option.Summary), before);
+ if (after != before) {
+ option.SetValue(so, after);
+ }
+
+ option.InnerOnGUI(so);
+
+ EditorGUILayout.EndToggleGroup();
+ EditorGUILayout.EndVertical();
+ }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadOptionsSection.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadOptionsSection.cs.meta
new file mode 100644
index 0000000..c370325
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadOptionsSection.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: 24379a407eff8494eac0f7841b70e574
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadOptionsSection.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadRunTab.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadRunTab.cs
new file mode 100644
index 0000000..302e07f
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadRunTab.cs
@@ -0,0 +1,1463 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using SingularityGroup.HotReload.DTO;
+using SingularityGroup.HotReload.Editor.Cli;
+using SingularityGroup.HotReload.EditorDependencies;
+using SingularityGroup.HotReload.Editor.Localization;
+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 ?? Translations.UI.TapToShowStacktrace;
+
+ // Get the error
+ string error = (errorString.Contains("error CS")
+ ? Translations.UI.CompileErrorMessage + ", "
+ : Translations.UI.UnsupportedChangeMessage + ", ") + Translations.UI.TapHereToSeeMore;
+ 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, Translations.UI.RunTabTitle, "forward", Translations.UI.RunTabTooltip) { }
+
+ 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);
+ }
+
+ var renderDebuggerInfo = Debugger.IsAttached && !CodePatcher.I.debuggerCompatibilityEnabled;
+ RenderIndicationPanel(!renderDebuggerInfo);
+ if (renderDebuggerInfo) {
+ RenderDebuggerAttachedInfo(false);
+ }
+ 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) {
+ if (Debugger.IsAttached && !CodePatcher.I.debuggerCompatibilityEnabled) {
+ return false;
+ }
+ 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 == Translations.Utility.UnsupportedChange || alertEntry.title == Translations.Utility.CompileError || alertEntry.title == Translations.Timeline.EventTitleFailedApplyingPatch) && 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);
+ if (kind == HotReloadSuggestionKind.EditorsWithoutHRRunning) {
+ HotReloadState.ShowedEditorsWithoutHR = true;
+ }
+ }
+ _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(string.Format(Translations.Timeline.LabelSuggestionsFormat, 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(Translations.Timeline.LabelTimeline, 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 = Translations.Timeline.MessageCompleteRegistration;
+ } else if (!currentState.running) {
+ text = Translations.Timeline.MessageUseStartButton;
+ } else if (enabledFilters.Count < 4 && HotReloadTimelineHelper.EventsTimeline.Count != 0) {
+ text = Translations.Timeline.MessageEnableFilters;
+ } else {
+ text = Translations.Timeline.MessageMakeCodeChanges;
+ }
+ GUILayout.Label(text, HotReloadWindowStyles.EmptyListText);
+ }
+ GUILayout.FlexibleSpace();
+ } else {
+ GUILayout.FlexibleSpace();
+ if (HotReloadTimelineHelper.EventsTimeline.Count > 0 && GUILayout.Button(Translations.Common.ButtonClear)) {
+ 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(Translations.License.TitleHotReloadLimited, HotReloadWindowStyles.H3CenteredTitleStyle);
+ EditorGUILayout.Space();
+ if (loginStatus.consumptionsUnavailableReason == ConsumptionsUnavailableReason.NetworkUnreachable) {
+ EditorGUILayout.HelpBox(Translations.Errors.ErrorNetworkIssue, MessageType.Warning);
+ } else if (loginStatus.consumptionsUnavailableReason == ConsumptionsUnavailableReason.UnrecoverableError) {
+ EditorGUILayout.HelpBox(Translations.Errors.ErrorContactSupport, MessageType.Error);
+ } else if (loginStatus.freeSessionFinished) {
+ var now = DateTime.UtcNow;
+ var sessionRefreshesAt = (now.AddDays(1).Date - now).Add(TimeSpan.FromMinutes(5));
+ var sessionRefreshString = sessionRefreshesAt.Hours > 0 ?
+ string.Format(Translations.Miscellaneous.DailySessionNextSessionHours, sessionRefreshesAt.Hours, sessionRefreshesAt.Minutes) :
+ string.Format(Translations.Miscellaneous.DailySessionNextSessionMinutes, sessionRefreshesAt.Minutes);
+ HotReloadGUIHelper.HelpBox(sessionRefreshString, MessageType.Warning, fontSize: 11);
+ } else if (loginStatus.freeSessionRunning && loginStatus.freeSessionEndTime != null) {
+ var sessionEndsAt = loginStatus.freeSessionEndTime.Value - DateTime.Now;
+ var sessionString = sessionEndsAt.Hours > 0 ?
+ string.Format(Translations.Miscellaneous.DailySessionTimeHoursLeft, sessionEndsAt.Hours, sessionEndsAt.Minutes) :
+ string.Format(Translations.Miscellaneous.DailySessionTimeMinutesLeft, sessionEndsAt.Minutes);
+ HotReloadGUIHelper.HelpBox(sessionString, MessageType.Info, fontSize: 11);
+ } else if (loginStatus.freeSessionEndTime == null) {
+ HotReloadGUIHelper.HelpBox(Translations.Miscellaneous.DailySessionStart, 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 ? Translations.UI.RecompileButtonLabel : "";
+ 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: Translations.Dialogs.DialogTitleRecompile,
+ message: Translations.Dialogs.DialogMessageRecompile,
+ ok: Translations.Common.ButtonRecompile.Trim(),
+ cancel: Translations.Common.ButtonNotNow);
+ HotReloadPrefs.RecompileDialogueShown = true;
+ if (!firstDialoguePass) {
+ return;
+ }
+ if (!ConfirmExitPlaymode(Translations.Dialogs.DialogMessageStopPlayMode)) {
+ 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: Translations.Dialogs.DialogTitleStopPlayMode,
+ message: message,
+ ok: Translations.Common.ButtonStopAndRecompile,
+ cancel: Translations.Common.ButtonCancel);
+ }
+
+ 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 ? Translations.UI.StartButtonLabel : "";
+ 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 ? Translations.UI.StopButtonLabel : "";
+ if (GUILayout.Button(new GUIContent(stopText, GUIHelper.GetInvertibleIcon(InvertibleIcon.Stop)), HotReloadWindowStyles.StopButton)) {
+ if (!EditorCodePatcher.StoppedServerRecently()) {
+ EditorCodePatcher.StopCodePatcher().Forget();
+ }
+ }
+ }
+ }
+
+ void RenderIndicationPanel(bool renderLicenseInfo = true) {
+ 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 (EditorCodePatcher.serverDownloader.Attempts > 0) {
+ RenderManualDownloadSection();
+ }
+ }
+
+ if (HotReloadWindowStyles.windowScreenWidth > Constants.ConsumptionsHideWidth
+ && HotReloadWindowStyles.windowScreenHeight > Constants.ConsumptionsHideHeight
+ && renderLicenseInfo
+ ) {
+ RenderLicenseInfo(currentState);
+ }
+ }
+
+ bool copiedPath = false;
+ bool openedDownloadUrl = false;
+ void RenderManualDownloadSection() {
+ var downloadUrl = ServerDownloader.GetDownloadUrl(HotReloadCli.controller);
+ var downloadPath = EditorCodePatcher.serverDownloader.GetBinaryPath(HotReloadCli.controller);
+
+ EditorGUILayout.Space();
+ HotReloadGUIHelper.HelpBox(
+ Translations.Timeline.ManualDownloadWarning,
+ MessageType.Warning, 11);
+
+ HotReloadGUIHelper.HelpBox(
+ string.Format(Translations.Timeline.ManualDownloadInfo, downloadPath),
+ MessageType.Info, 11);
+
+ using (new EditorGUILayout.HorizontalScope()) {
+ if (GUILayout.Button(Translations.Timeline.ManualDownloadButtonCopyToClipboard + (copiedPath ? " ✓" : ""))) {
+ GUIUtility.systemCopyBuffer = downloadPath;
+ copiedPath = true;
+ }
+ if (GUILayout.Button(Translations.Timeline.ManualDownloadButtonOpenDownloadUrl + (openedDownloadUrl ? " ✓" : ""))) {
+ Application.OpenURL(downloadUrl);
+ openedDownloadUrl = true;
+ }
+ }
+ if (GUILayout.Button(Translations.Timeline.ManualDownloadButtonComplete)) {
+ EditorCodePatcher.downloadCancelToken?.Cancel();
+ copiedPath = false;
+ openedDownloadUrl = false;
+ }
+ OpenURLButton.Render(Translations.Timeline.ManualDownloadButtonContactSupport, Constants.ContactURL);
+ EditorGUILayout.Space();
+ }
+
+ 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 = string.Format(Translations.UI.TrialLicenseMessage, loginStatus.licenseExpiresAt.ToShortDateString());
+ messageType = MessageType.Info;
+ } else if (loginStatus.isIndieLicense) {
+ if (verbose) {
+ message = Translations.UI.IndieLicenseMessage;
+ messageType = MessageType.Info;
+ customGUI = () => {
+ if (loginStatus.licenseExpiresAt.Date != DateTime.MaxValue.Date) {
+ EditorGUILayout.LabelField(string.Format(Translations.UI.LicenseRenewalMessage, 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 = Translations.UI.BusinessLicenseMessage;
+ 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(Translations.Common.ButtonHide, style)) {
+ HotReloadPrefs.ErrorHidden = true;
+ }
+ }
+ }
+ } else if (content != null) {
+ EditorGUILayout.LabelField(content);
+ EditorGUILayout.Space();
+ } else {
+ EditorGUILayout.LabelField(message);
+ EditorGUILayout.Space();
+ }
+ customGUI?.Invoke();
+ }
+ }
+
+ internal static void RenderBusinessLicenseInfo(GUIStyle style) {
+ GUILayout.Space(8);
+ using (new EditorGUILayout.HorizontalScope()) {
+ EditorGUILayout.HelpBox(Translations.License.LicenseErrorAssetStorePro, MessageType.Info);
+ if (Event.current.type == EventType.Repaint) {
+ style.fixedHeight = GUILayoutUtility.GetLastRect().height;
+ }
+ if (GUILayout.Button(Translations.Common.ButtonUpgrade, style)) {
+ Application.OpenURL(Constants.ProductPurchaseBusinessURL);
+ }
+ }
+ }
+
+ internal static void RenderDebuggerAttachedInfo(bool isPopup) {
+ GUILayout.Space(8);
+ var autoRefreshDisabled = AutoRefreshSettingChecker.IsUserAutoRefreshDisabled();
+ var msg = autoRefreshDisabled ? Translations.Suggestions.DebuggerAttachedMessagePaused : Translations.Suggestions.DebuggerAttachedMessageAutoRecompile;
+ using (new EditorGUILayout.VerticalScope()) {
+ var _fontSize = EditorStyles.helpBox.fontSize;
+ try {
+ EditorStyles.helpBox.fontSize = 12;
+ // empty label field to measure width
+ EditorGUILayout.LabelField(GUIContent.none, new GUILayoutOption[]{GUILayout.Height(0)});
+ var lastRectWidth = GUILayoutUtility.GetLastRect().width;
+ EditorStyles.helpBox.fixedHeight = EditorStyles.helpBox.CalcHeight(new GUIContent(msg), lastRectWidth) + 15;
+ EditorStyles.helpBox.fixedWidth = lastRectWidth;
+ EditorGUILayout.HelpBox(msg, MessageType.Info);
+ // to account for added height
+ GUILayout.Space(isPopup ? 45 : 30);
+ using (new EditorGUILayout.HorizontalScope()) {
+ if (GUILayout.Button("Docs")) {
+ Application.OpenURL(Constants.DebuggerURL);
+ }
+ if (GUILayout.Button("Open Settings") && HotReloadWindow.Current) {
+ HotReloadWindow.Current.SelectTab(typeof(HotReloadSettingsTab));
+ }
+ }
+ } finally {
+ EditorStyles.helpBox.fixedHeight = 0;
+ EditorStyles.helpBox.fixedWidth = 0;
+ EditorStyles.helpBox.fontSize = _fontSize;
+ }
+ }
+ }
+
+ internal static void RenderIndieLicenseInfo(GUIStyle style) {
+ GUILayout.Space(8);
+ using (new EditorGUILayout.HorizontalScope()) {
+ EditorGUILayout.HelpBox(Translations.License.LicenseErrorUnityPlusIndie, MessageType.Info);
+ if (Event.current.type == EventType.Repaint) {
+ style.fixedHeight = GUILayoutUtility.GetLastRect().height;
+ }
+ if (GUILayout.Button(Translations.Common.ButtonUpgrade, style)) {
+ Application.OpenURL(Constants.ProductPurchaseURL);
+ }
+ }
+ }
+
+ internal static Dictionary _licenseErrorData;
+ internal static Dictionary LicenseErrorData => _licenseErrorData ?? (_licenseErrorData = new Dictionary {
+ { "DeviceNotLicensedException", new LicenseErrorData(description: Translations.License.LicenseErrorDeviceInUse, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport) },
+ { "DeviceBlacklistedException", new LicenseErrorData(description: Translations.License.LicenseErrorDeviceBlacklisted) },
+ { "DateHeaderInvalidException", new LicenseErrorData(description: Translations.License.LicenseErrorIncorrectClock) },
+ { "DateTimeCheatingException", new LicenseErrorData(description: Translations.License.LicenseErrorIncorrectClock) },
+ { "LicenseActivationException", new LicenseErrorData(description: Translations.License.LicenseErrorActivation, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport) },
+ { "LicenseDeletedException", new LicenseErrorData(description: Translations.License.LicenseErrorDeleted, showBuyButton: true, buyButtonText: Translations.License.LicenseButtonGetLicense, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport) },
+ { "LicenseDisabledException", new LicenseErrorData(description: Translations.License.LicenseErrorDisabled, showBuyButton: true, buyButtonText: Translations.License.LicenseButtonGetLicense, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport) },
+ { "LicenseExpiredException", new LicenseErrorData(description: Translations.License.LicenseErrorExpired, showBuyButton: true, buyButtonText: Translations.License.LicenseButtonUpgradeLicense, showManageLicenseButton: true, manageLicenseButtonText: Translations.License.LicenseButtonManageLicense) },
+ { "LicenseInactiveException", new LicenseErrorData(description: Translations.License.LicenseErrorInactive) },
+ { "LocalLicenseException", new LicenseErrorData(description: Translations.License.LicenseErrorCorrupted) },
+ // Note: obsolete
+ { "MissingParametersException", new LicenseErrorData(description: "An account already exists for this device. Please login with your existing email/password.", showBuyButton: true, buyButtonText: Translations.License.LicenseButtonGetLicense) },
+ { "NetworkException", new LicenseErrorData(description: Translations.License.LicenseErrorNetwork, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport) },
+ { "TrialLicenseExpiredException", new LicenseErrorData(description: Translations.License.LicenseErrorTrialExpired, showBuyButton: true, buyButtonText: Translations.License.LicenseButtonUpgradeLicense) },
+ { "InvalidCredentialException", new LicenseErrorData(description: Translations.License.LicenseErrorInvalidCredentials) },
+ // 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: Translations.License.LicenseErrorIncompatible, showManageLicenseButton: true, manageLicenseButtonText: Translations.License.LicenseButtonManageLicense) },
+ });
+ internal static LicenseErrorData defaultLicenseErrorData = new LicenseErrorData(description: Translations.License.LicenseErrorDefault, showSupportButton: true, supportButtonText: Translations.License.LicenseButtonContactSupport);
+
+ internal static string GetMessageFromError(HotReloadRunTabState currentState, string error) {
+ if (PackageConst.IsAssetStoreBuild && error == "TrialLicenseExpiredException") {
+ return Translations.License.LicenseErrorAssetStorePro;
+ }
+ 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: Translations.License.LicenseButtonGetLicense);
+ }
+ 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, Translations.License.TitleHotReloadLicense, 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, Translations.License.PromoCodesTitle, true, HotReloadWindowStyles.FoldoutStyle);
+ if (!HotReloadPrefs.ShowPromoCodes) {
+ return;
+ }
+ if (promoCodeActivatedThisSession) {
+ EditorGUILayout.HelpBox(Translations.License.MessagePromoCodeActivated, MessageType.Info);
+ } else {
+ if (promoCodeError != null && promoCodeErrorType != MessageType.None) {
+ EditorGUILayout.HelpBox(promoCodeError, promoCodeErrorType);
+ }
+ EditorGUILayout.LabelField(Translations.Common.LabelPromoCode);
+ _pendingPromoCode = EditorGUILayout.TextField(_pendingPromoCode);
+ EditorGUILayout.Space();
+
+ using (new EditorGUI.DisabledScope(_requestingActivatePromoCode)) {
+ if (GUILayout.Button(Translations.Common.ButtonActivatePromoCode, 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 = Translations.Errors.ErrorPromoCodeActivation;
+ 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 Translations.Errors.ErrorPromoCodeInvalid;
+ case PromoCodeErrorType.PROMO_CODE_CLAIMED: return Translations.Errors.ErrorPromoCodeUsed;
+ case PromoCodeErrorType.PROMO_CODE_EXPIRED: return Translations.Errors.ErrorPromoCodeExpired;
+ case PromoCodeErrorType.LICENSE_ALREADY_EXTENDED: return Translations.Errors.ErrorLicenseExtended;
+ case PromoCodeErrorType.CONDITIONAL_CHECK_FAILED: return Translations.Errors.ErrorPromoCodeActivation;
+ case PromoCodeErrorType.NONE: return Translations.Errors.ErrorPromoCodeNetwork;
+ 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(Translations.Common.LabelEmail);
+ GUI.SetNextControlName("email");
+ _pendingEmail = EditorGUILayout.TextField(string.IsNullOrEmpty(_pendingEmail) ? HotReloadPrefs.LicenseEmail : _pendingEmail);
+ _pendingEmail = _pendingEmail.Trim();
+
+ EditorGUILayout.LabelField(Translations.Common.LabelPassword);
+ 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 = Translations.Common.ButtonLogin;
+ }
+ 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(Translations.Errors.ErrorEnterPassword, 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 Translations.Errors.ErrorEnterEmail;
+ } else if (!EditorWindowHelper.IsValidEmailAddress(email)) {
+ return Translations.Errors.ErrorValidEmail;
+ } else if (email.Contains("+")) {
+ return Translations.Errors.ErrorMailExtensions;
+ }
+ return null;
+ }
+
+ public static void RenderLogout(HotReloadRunTabState currentState) {
+ if (currentState.loginStatus?.isLicensed != true) {
+ return;
+ }
+ if (GUILayout.Button(Translations.Common.ButtonLogout, 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(Translations.Miscellaneous.LinkForgotPassword, 12, FontStyle.Normal, TextAnchor.MiddleLeft, color)) {
+ if (EditorUtility.DisplayDialog(Translations.Dialogs.DialogTitleRecoverPassword, Translations.Dialogs.DialogMessageRecoverPassword, Translations.Common.ButtonOpenInBrowser, Translations.Common.ButtonCancel)) {
+ 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;
+ }
+ }
+}
+
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadRunTab.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadRunTab.cs.meta
new file mode 100644
index 0000000..22d43bf
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadRunTab.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: 38d0877009d34a9458f7d169d7f1b6a7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadRunTab.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadSettingsTab.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadSettingsTab.cs
new file mode 100644
index 0000000..3589f3f
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadSettingsTab.cs
@@ -0,0 +1,945 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using SingularityGroup.HotReload.DTO;
+using SingularityGroup.HotReload.Editor.Cli;
+using SingularityGroup.HotReload.Editor.Localization;
+using RuntimeLocalization = SingularityGroup.HotReload.Localization;
+using UnityEditor;
+using UnityEngine;
+using EditorGUI = UnityEditor.EditorGUI;
+
+namespace SingularityGroup.HotReload.Editor {
+ internal struct HotReloadSettingsTabState {
+ public readonly bool running;
+ public readonly bool trialLicense;
+ public readonly LoginStatusResponse loginStatus;
+ public readonly bool isServerHealthy;
+ public readonly bool registrationRequired;
+
+ public HotReloadSettingsTabState(
+ bool running,
+ bool trialLicense,
+ LoginStatusResponse loginStatus,
+ bool isServerHealthy,
+ bool registrationRequired
+ ) {
+ this.running = running;
+ this.trialLicense = trialLicense;
+ this.loginStatus = loginStatus;
+ this.isServerHealthy = isServerHealthy;
+ this.registrationRequired = registrationRequired;
+ }
+ }
+
+ internal class HotReloadSettingsTab : HotReloadTabBase {
+ private readonly HotReloadOptionsSection optionsSection;
+
+ // cached because changing built target triggers C# domain reload
+ // Also I suspect selectedBuildTargetGroup has chance to freeze Unity for several seconds (unconfirmed).
+ private readonly Lazy currentBuildTarget = new Lazy(
+ () => BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget));
+
+ private readonly Lazy isCurrentBuildTargetSupported = new Lazy(() => {
+ var target = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
+ return HotReloadBuildHelper.IsMonoSupported(target);
+ });
+
+ // Resources.Load uses cache, so it's safe to call it every frame.
+ // Retrying Load every time fixes an issue where you import the package and constructor runs, but resources aren't loadable yet.
+ private Texture iconCheck => Resources.Load("icon_check_circle");
+ private Texture iconWarning => Resources.Load("icon_warning_circle");
+
+ [SuppressMessage("ReSharper", "Unity.UnknownResource")] // Rider doesn't check packages
+ public HotReloadSettingsTab(HotReloadWindow window) : base(window,
+ Translations.Settings.SettingsTitle,
+ "_Popup",
+ Translations.OnDevice.OnDeviceHeadline) {
+ optionsSection = new HotReloadOptionsSection();
+ }
+
+ private GUIStyle headlineStyle;
+ private GUIStyle paddedStyle;
+
+ private Vector2 _settingsTabScrollPos;
+
+ HotReloadSettingsTabState currentState;
+ public override void OnGUI() {
+ // HotReloadAboutTabState ensures rendering is consistent between Layout and Repaint calls
+ // Without it errors like this happen:
+ // ArgumentException: Getting control 2's position in a group with only 2 controls when doing repaint
+ // See thread for more context: https://answers.unity.com/questions/17718/argumentexception-getting-control-2s-position-in-a.html
+ if (Event.current.type == EventType.Layout) {
+ currentState = new HotReloadSettingsTabState(
+ running: EditorCodePatcher.Running,
+ trialLicense: EditorCodePatcher.Status != null && (EditorCodePatcher.Status?.isTrial == true),
+ loginStatus: EditorCodePatcher.Status,
+ isServerHealthy: ServerHealthCheck.I.IsServerHealthy,
+ registrationRequired: RedeemLicenseHelper.I.RegistrationRequired
+ );
+ }
+ using (var scope = new EditorGUILayout.ScrollViewScope(_settingsTabScrollPos, GUI.skin.horizontalScrollbar, GUI.skin.verticalScrollbar, GUILayout.MaxHeight(Math.Max(HotReloadWindowStyles.windowScreenHeight, 800)), GUILayout.MaxWidth(Math.Max(HotReloadWindowStyles.windowScreenWidth, 800)))) {
+ _settingsTabScrollPos.x = scope.scrollPosition.x;
+ _settingsTabScrollPos.y = scope.scrollPosition.y;
+ using (new EditorGUILayout.VerticalScope(HotReloadWindowStyles.DynamicSectionHelpTab)) {
+ GUILayout.Space(10);
+ if (!EditorCodePatcher.LoginNotRequired
+ && !currentState.registrationRequired
+ // Delay showing login in settings to not confuse users that they need to login to use Free trial
+ && (HotReloadPrefs.RateAppShown
+ || PackageConst.IsAssetStoreBuild)
+ ) {
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionOuterBoxCompact)) {
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionInnerBoxWide)) {
+ using (new EditorGUILayout.VerticalScope()) {
+ RenderLicenseInfoSection();
+ }
+ }
+ }
+ }
+
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionOuterBoxCompact)) {
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionInnerBoxWide)) {
+ using (new EditorGUILayout.VerticalScope()) {
+ HotReloadPrefs.ShowConfiguration = EditorGUILayout.Foldout(HotReloadPrefs.ShowConfiguration, Translations.Settings.SettingsConfiguration, true, HotReloadWindowStyles.FoldoutStyle);
+ if (HotReloadPrefs.ShowConfiguration) {
+ EditorGUILayout.Space();
+
+ // main section
+ RenderUnityAutoRefresh();
+ using (new EditorGUI.DisabledScope(!EditorCodePatcher.autoRecompileUnsupportedChangesSupported)) {
+ RenderAutoRecompileUnsupportedChanges();
+ if (HotReloadPrefs.AutoRecompileUnsupportedChanges && EditorCodePatcher.autoRecompileUnsupportedChangesSupported) {
+ using (new EditorGUILayout.VerticalScope(paddedStyle ?? (paddedStyle = new GUIStyle { padding = new RectOffset(20, 0, 0, 0) }))) {
+ RenderAutoRecompileUnsupportedChangesImmediately();
+ RenderAutoRecompileUnsupportedChangesOnExitPlayMode();
+ RenderAutoRecompileUnsupportedChangesInPlayMode();
+ RenderAutoRecompileInspectorFieldEdits();
+ RenderAutoRecompilePartiallyUnsupportedChanges();
+ RenderDisplayNewMonobehaviourMethodsAsPartiallySupported();
+ RenderAutoRecompileUnsupportedChangesInEditMode();
+ }
+ }
+ EditorGUILayout.Space();
+ }
+ RenderAssetRefresh();
+ if (HotReloadPrefs.AllAssetChanges) {
+ using (new EditorGUILayout.VerticalScope(paddedStyle ?? (paddedStyle = new GUIStyle { padding = new RectOffset(20, 0, 0, 0) }))) {
+ RenderIncludeShaderChanges();
+ }
+
+ EditorGUILayout.Space();
+ }
+ RenderDebuggerCompatibility();
+
+ // // fields
+ // RenderShowFeatures();
+ // using (new EditorGUILayout.VerticalScope(paddedStyle ?? (paddedStyle = new GUIStyle { padding = new RectOffset(20, 0, 0, 0) }))) {
+ // RenderShowApplyfieldInitializerEditsToExistingClassInstances();
+ //
+ // EditorGUILayout.Space();
+ // }
+
+ // visual feedback
+ if (EditorWindowHelper.supportsNotifications) {
+ RenderShowNotifications();
+ using (new EditorGUILayout.VerticalScope(paddedStyle ?? (paddedStyle = new GUIStyle { padding = new RectOffset(20, 0, 0, 0) }))) {
+ RenderShowPatchingNotifications();
+ RenderShowCompilingUnsupportedNotifications();
+ }
+
+ EditorGUILayout.Space();
+ }
+
+ // misc
+ RenderMiscHeader();
+ using (new EditorGUILayout.VerticalScope(paddedStyle ?? (paddedStyle = new GUIStyle { padding = new RectOffset(20, 0, 0, 0) }))) {
+ RenderAutoClearTimeline();
+ RenderAutostart();
+ RenderConsoleWindow();
+
+ EditorGUILayout.Space();
+ }
+
+ EditorGUILayout.Space();
+ using (new EditorGUILayout.HorizontalScope()) {
+ GUILayout.FlexibleSpace();
+ HotReloadWindow.RenderShowOnStartup();
+ }
+ }
+ }
+ }
+ }
+
+ if (!EditorCodePatcher.LoginNotRequired && currentState.trialLicense && currentState.running) {
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionOuterBoxCompact)) {
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionInnerBoxWide)) {
+ using (new EditorGUILayout.VerticalScope()) {
+ RenderPromoCodeSection();
+ }
+ }
+ }
+ }
+
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionOuterBoxCompact)) {
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionInnerBoxWide)) {
+ using (new EditorGUILayout.VerticalScope()) {
+ RenderOnDevice();
+ }
+ }
+ }
+
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionOuterBoxCompact)) {
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.SectionInnerBoxWide)) {
+ using (new EditorGUILayout.VerticalScope()) {
+ HotReloadPrefs.ShowAdvanced = EditorGUILayout.Foldout(HotReloadPrefs.ShowAdvanced, Translations.Settings.SettingsAdvanced, true, HotReloadWindowStyles.FoldoutStyle);
+ if (HotReloadPrefs.ShowAdvanced) {
+ EditorGUILayout.Space();
+
+ DeactivateHotReload();
+ DisableDetailedErrorReporting();
+ PauseHotReloadInEditMode();
+#if UNITY_EDITOR_WIN
+ if (PackageConst.DefaultLocale == RuntimeLocalization.Locale.English) {
+ UseWatchman();
+ }
+#endif
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void RenderUnityAutoRefresh() {
+ var newSettings = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleManageAutoRefresh), HotReloadPrefs.AllowDisableUnityAutoRefresh);
+ if (newSettings != HotReloadPrefs.AllowDisableUnityAutoRefresh) {
+ HotReloadPrefs.AllowDisableUnityAutoRefresh = newSettings;
+ }
+ string toggleDescription;
+ if (HotReloadPrefs.AllowDisableUnityAutoRefresh) {
+ toggleDescription = Translations.Settings.SettingsManageAutoRefreshOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsManageAutoRefreshOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ EditorGUILayout.Space(6f);
+ }
+
+ void RenderAssetRefresh() {
+ var newSettings = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleAssetRefresh), HotReloadPrefs.AllAssetChanges);
+ if (newSettings != HotReloadPrefs.AllAssetChanges) {
+ HotReloadPrefs.AllAssetChanges = newSettings;
+ // restart when setting changes
+ if (ServerHealthCheck.I.IsServerHealthy) {
+ var restartServer = EditorUtility.DisplayDialog(Translations.Dialogs.DialogTitleRestartServer,
+ Translations.Dialogs.DialogMessageRestartAssetRefresh,
+ Translations.Dialogs.DialogButtonRestartHotReload, Translations.Dialogs.DialogButtonDontRestart);
+ if (restartServer) {
+ EditorCodePatcher.RestartCodePatcher().Forget();
+ }
+ }
+ }
+ string toggleDescription;
+ if (HotReloadPrefs.AllAssetChanges) {
+ toggleDescription = Translations.Settings.SettingsAssetRefreshOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsAssetRefreshOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ EditorGUILayout.Space(6f);
+ }
+
+ void RenderDebuggerCompatibility() {
+ var newSettings = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleDebuggerCompatibility), HotReloadPrefs.AutoDisableHotReloadWithDebugger);
+ if (newSettings != HotReloadPrefs.AutoDisableHotReloadWithDebugger) {
+ HotReloadPrefs.AutoDisableHotReloadWithDebugger = newSettings;
+ CodePatcher.I.debuggerCompatibilityEnabled = !HotReloadPrefs.AutoDisableHotReloadWithDebugger;
+ }
+ string toggleDescription;
+ if (HotReloadPrefs.AutoDisableHotReloadWithDebugger) {
+ toggleDescription = Translations.Settings.SettingsDebuggerCompatibilityOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsDebuggerCompatibilityOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ EditorGUILayout.Space(6f);
+ }
+
+ void RenderIncludeShaderChanges() {
+ HotReloadPrefs.IncludeShaderChanges = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleRefreshShaders), HotReloadPrefs.IncludeShaderChanges);
+ string toggleDescription;
+ if (HotReloadPrefs.IncludeShaderChanges) {
+ toggleDescription = Translations.Settings.SettingsRefreshShadersOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsRefreshShadersOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ }
+
+ void RenderConsoleWindow() {
+ if (!HotReloadCli.CanOpenInBackground) {
+ return;
+ }
+ var newSettings = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleHideConsole), HotReloadPrefs.DisableConsoleWindow);
+ if (newSettings != HotReloadPrefs.DisableConsoleWindow) {
+ HotReloadPrefs.DisableConsoleWindow = newSettings;
+ // restart when setting changes
+ if (ServerHealthCheck.I.IsServerHealthy) {
+ var restartServer = EditorUtility.DisplayDialog(Translations.Dialogs.DialogTitleRestartServer,
+ Translations.Dialogs.DialogMessageRestartConsoleWindow,
+ Translations.Dialogs.DialogButtonRestartServer, Translations.Dialogs.DialogButtonDontRestart);
+ if (restartServer) {
+ EditorCodePatcher.RestartCodePatcher().Forget();
+ }
+ }
+ }
+ string toggleDescription;
+ if (HotReloadPrefs.DisableConsoleWindow) {
+ toggleDescription = Translations.Settings.SettingsHideConsoleOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsHideConsoleOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ EditorGUILayout.Space(6f);
+ }
+
+ void DeactivateHotReload() {
+ var newSettings = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleDeactivate), HotReloadPrefs.DeactivateHotReload);
+ if (newSettings != HotReloadPrefs.DeactivateHotReload) {
+ DeactivateHotReloadInner(newSettings);
+ }
+ string toggleDescription;
+ if (HotReloadPrefs.DeactivateHotReload) {
+ toggleDescription = Translations.Settings.SettingsDeactivatedOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsDeactivatedOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ EditorGUILayout.Space(6f);
+ }
+
+ void DisableDetailedErrorReporting() {
+ var newSettings = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleDisableErrorReporting), HotReloadPrefs.DisableDetailedErrorReporting);
+ DisableDetailedErrorReportingInner(newSettings);
+ string toggleDescription;
+ if (HotReloadPrefs.DisableDetailedErrorReporting) {
+ toggleDescription = Translations.Settings.SettingsDisableErrorReportingOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsDisableErrorReportingOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ EditorGUILayout.Space(6f);
+ }
+
+ void PauseHotReloadInEditMode() {
+ HotReloadPrefs.PauseHotReloadInEditMode = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.TogglePauseEditMode), HotReloadPrefs.PauseHotReloadInEditMode);
+ string toggleDescription;
+ if (HotReloadPrefs.PauseHotReloadInEditMode) {
+ toggleDescription = Translations.Settings.SettingsPauseEditModeOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsPauseEditModeOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ EditorGUILayout.Space(6f);
+ }
+
+#if UNITY_EDITOR_WIN
+ void UseWatchman() {
+ HotReloadPrefs.UseWatchman = EditorGUILayout.BeginToggleGroup(new GUIContent("Use watchman"), HotReloadPrefs.UseWatchman);
+ string toggleDescription;
+ if (HotReloadPrefs.UseWatchman) {
+ toggleDescription = "Use watchman file watcher";
+ } else {
+ toggleDescription = "Use default file watcher";
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ EditorGUILayout.Space(6f);
+ }
+#endif
+
+ public static void DisableDetailedErrorReportingInner(bool newSetting) {
+ if (newSetting == HotReloadPrefs.DisableDetailedErrorReporting) {
+ return;
+ }
+ HotReloadPrefs.DisableDetailedErrorReporting = newSetting;
+ // restart when setting changes
+ if (ServerHealthCheck.I.IsServerHealthy) {
+ var restartServer = EditorUtility.DisplayDialog(Translations.Dialogs.DialogTitleRestartServer,
+ Translations.Dialogs.DialogMessageRestartErrorReporting,
+ Translations.Dialogs.DialogButtonRestartServer, Translations.Dialogs.DialogButtonDontRestart);
+ if (restartServer) {
+ EditorCodePatcher.RestartCodePatcher().Forget();
+ }
+ }
+ }
+
+ static void DeactivateHotReloadInner(bool deactivate) {
+ var confirmed = !deactivate || EditorUtility.DisplayDialog(Translations.Dialogs.DialogTitleDeactivate,
+ Translations.Dialogs.DialogMessageDeactivate,
+ Translations.Dialogs.DialogButtonDeactivate, Translations.Common.ButtonCancel);
+ if (confirmed) {
+ HotReloadPrefs.DeactivateHotReload = deactivate;
+ if (deactivate) {
+ EditorCodePatcher.StopCodePatcher(recompileOnDone: true).Forget();
+ } else {
+ HotReloadRunTab.Recompile();
+ }
+ }
+ }
+
+ void RenderAutoClearTimeline() {
+ var newSettings = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleAutoClearTimeline), HotReloadPrefs.AutoClearTimeline);
+ if (newSettings != HotReloadPrefs.AutoClearTimeline) {
+ HotReloadPrefs.AutoClearTimeline = newSettings;
+ }
+ string toggleDescription;
+ if (HotReloadPrefs.AutoClearTimeline) {
+ toggleDescription = Translations.Settings.SettingsAutoClearTimelineOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsAutoClearTimelineOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ EditorGUILayout.Space();
+ }
+
+ void RenderAutostart() {
+ var newSettings = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleAutostart), HotReloadPrefs.LaunchOnEditorStart);
+ if (newSettings != HotReloadPrefs.LaunchOnEditorStart) {
+ HotReloadPrefs.LaunchOnEditorStart = newSettings;
+ }
+ string toggleDescription;
+ if (HotReloadPrefs.LaunchOnEditorStart) {
+ toggleDescription = Translations.Settings.SettingsAutostartOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsAutostartOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ EditorGUILayout.Space();
+ }
+
+ void RenderShowNotifications() {
+ EditorGUILayout.Space(10f);
+ GUILayout.Label(Translations.Settings.SettingsVisualFeedback, HotReloadWindowStyles.NotificationsTitleStyle);
+ EditorGUILayout.Space(10f);
+
+ if (!EditorWindowHelper.supportsNotifications && !UnitySettingsHelper.I.playmodeTintSupported) {
+ var toggleDescription = Translations.Settings.SettingsIndicationsUnsupported;
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ }
+ }
+
+ // void RenderShowFields() {
+ // EditorGUILayout.Space(14f);
+ // GUILayout.Label("Fields", HotReloadWindowStyles.NotificationsTitleStyle);
+ // }
+
+ void RenderMiscHeader() {
+ EditorGUILayout.Space(10f);
+ GUILayout.Label(Translations.Settings.SettingsMisc, HotReloadWindowStyles.NotificationsTitleStyle);
+ EditorGUILayout.Space(10f);
+ }
+
+ void RenderShowPatchingNotifications() {
+ HotReloadPrefs.ShowPatchingNotifications = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.TogglePatchingIndication), HotReloadPrefs.ShowPatchingNotifications);
+ string toggleDescription;
+ if (!EditorWindowHelper.supportsNotifications) {
+ toggleDescription = Translations.Settings.SettingsPatchingIndicationUnsupported;
+ } else if (!HotReloadPrefs.ShowPatchingNotifications) {
+ toggleDescription = Translations.Settings.SettingsPatchingIndicationOff;
+ } else {
+ toggleDescription = Translations.Settings.SettingsPatchingIndicationOn;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ }
+
+ // void RenderShowApplyfieldInitializerEditsToExistingClassInstances() {
+ // var newSetting = EditorGUILayout.BeginToggleGroup(new GUIContent("Apply field initializer edits to existing class instances"), HotReloadPrefs.ApplyFieldInitiailzerEditsToExistingClassInstances);
+ // ApplyApplyFieldInitializerEditsToExistingClassInstances(newSetting);
+ // string toggleDescription;
+ // if (HotReloadPrefs.ApplyFieldInitiailzerEditsToExistingClassInstances) {
+ // toggleDescription = "New field initializers with constant value will update field value of existing objects.";
+ // } else {
+ // toggleDescription = "New field initializers will not modify existing objects.";
+ // }
+ // EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ // EditorGUILayout.EndToggleGroup();
+ // }
+
+ [Obsolete(Translations.MenuItems.NotImplementedObsolete)]
+ public static void ApplyApplyFieldInitializerEditsToExistingClassInstances(bool newSetting) {
+ if (newSetting != HotReloadPrefs.ApplyFieldInitiailzerEditsToExistingClassInstances) {
+ HotReloadPrefs.ApplyFieldInitiailzerEditsToExistingClassInstances = newSetting;
+ // restart when setting changes
+ if (ServerHealthCheck.I.IsServerHealthy) {
+ var restartServer = EditorUtility.DisplayDialog(Translations.Dialogs.DialogTitleRestartServer,
+ Translations.Dialogs.DialogMessageRestartFieldInitializer,
+ Translations.Dialogs.DialogButtonRestartServer, Translations.Dialogs.DialogButtonDontRestart);
+ if (restartServer) {
+ EditorCodePatcher.RestartCodePatcher().Forget();
+ }
+ }
+ }
+ }
+
+ void RenderShowCompilingUnsupportedNotifications() {
+ HotReloadPrefs.ShowCompilingUnsupportedNotifications = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleCompilingIndication), HotReloadPrefs.ShowCompilingUnsupportedNotifications);
+ string toggleDescription;
+ if (!EditorWindowHelper.supportsNotifications) {
+ toggleDescription = Translations.Settings.SettingsCompilingIndicationUnsupported;
+ } else if (!HotReloadPrefs.ShowCompilingUnsupportedNotifications) {
+ toggleDescription = Translations.Settings.SettingsCompilingIndicationOff;
+ } else {
+ toggleDescription = Translations.Settings.SettingsCompilingIndicationOn;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ }
+
+ void RenderAutoRecompileUnsupportedChanges() {
+ HotReloadPrefs.AutoRecompileUnsupportedChanges = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleAutoRecompile), HotReloadPrefs.AutoRecompileUnsupportedChanges && EditorCodePatcher.autoRecompileUnsupportedChangesSupported);
+ string toggleDescription;
+ if (!EditorCodePatcher.autoRecompileUnsupportedChangesSupported) {
+ toggleDescription = Translations.Settings.SettingsAutoRecompileUnsupported;
+ } else if (HotReloadPrefs.AutoRecompileUnsupportedChanges) {
+ toggleDescription = Translations.Settings.SettingsAutoRecompileOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsAutoRecompileOff;
+ }
+ if (!HotReloadPrefs.AutoRecompileUnsupportedChangesInEditMode && !HotReloadPrefs.AutoRecompileUnsupportedChangesInPlayMode) {
+ HotReloadPrefs.AutoRecompileUnsupportedChangesInEditMode = true;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ }
+
+ void RenderAutoRecompileInspectorFieldEdits() {
+ HotReloadPrefs.AutoRecompileInspectorFieldsEdit = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleAutoRecompileInspector), HotReloadPrefs.AutoRecompileInspectorFieldsEdit);
+ string toggleDescription;
+ if (HotReloadPrefs.AutoRecompileInspectorFieldsEdit) {
+ toggleDescription = Translations.Settings.SettingsAutoRecompileInspectorOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsAutoRecompileInspectorOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ }
+
+ void RenderAutoRecompilePartiallyUnsupportedChanges() {
+ HotReloadPrefs.AutoRecompilePartiallyUnsupportedChanges = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleAutoRecompilePartial), HotReloadPrefs.AutoRecompilePartiallyUnsupportedChanges);
+ string toggleDescription;
+ if (HotReloadPrefs.AutoRecompilePartiallyUnsupportedChanges) {
+ toggleDescription = Translations.Settings.SettingsAutoRecompilePartialOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsAutoRecompilePartialOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ }
+
+ void RenderDisplayNewMonobehaviourMethodsAsPartiallySupported() {
+ HotReloadPrefs.DisplayNewMonobehaviourMethodsAsPartiallySupported = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleDisplayMonobehaviour), HotReloadPrefs.DisplayNewMonobehaviourMethodsAsPartiallySupported);
+ string toggleDescription;
+ if (HotReloadPrefs.DisplayNewMonobehaviourMethodsAsPartiallySupported) {
+ toggleDescription = Translations.Settings.SettingsDisplayMonobehaviourOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsDisplayMonobehaviourOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ }
+
+ void RenderAutoRecompileUnsupportedChangesImmediately() {
+ HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleRecompileImmediately), HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately);
+ string toggleDescription;
+ if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately) {
+ toggleDescription = Translations.Settings.SettingsRecompileImmediatelyOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsRecompileImmediatelyOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ }
+
+ void RenderAutoRecompileUnsupportedChangesInPlayMode() {
+ HotReloadPrefs.AutoRecompileUnsupportedChangesInPlayMode = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleRecompilePlayMode), HotReloadPrefs.AutoRecompileUnsupportedChangesInPlayMode);
+ string toggleDescription;
+ if (HotReloadPrefs.AutoRecompileUnsupportedChangesInPlayMode) {
+ toggleDescription = Translations.Settings.SettingsRecompilePlayModeOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsRecompilePlayModeOff;
+ }
+ if (!HotReloadPrefs.AutoRecompileUnsupportedChangesInEditMode && !HotReloadPrefs.AutoRecompileUnsupportedChangesInPlayMode) {
+ HotReloadPrefs.AutoRecompileUnsupportedChanges = false;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ }
+
+ void RenderAutoRecompileUnsupportedChangesInEditMode() {
+ HotReloadPrefs.AutoRecompileUnsupportedChangesInEditMode = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleRecompileEditMode), HotReloadPrefs.AutoRecompileUnsupportedChangesInEditMode);
+ string toggleDescription;
+ if (HotReloadPrefs.AutoRecompileUnsupportedChangesInEditMode) {
+ toggleDescription = Translations.Settings.SettingsRecompileEditModeOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsRecompileEditModeOff;
+ }
+ if (!HotReloadPrefs.AutoRecompileUnsupportedChangesInEditMode && !HotReloadPrefs.AutoRecompileUnsupportedChangesInPlayMode) {
+ HotReloadPrefs.AutoRecompileUnsupportedChanges = false;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ }
+
+ void RenderAutoRecompileUnsupportedChangesOnExitPlayMode() {
+ HotReloadPrefs.AutoRecompileUnsupportedChangesOnExitPlayMode = EditorGUILayout.BeginToggleGroup(new GUIContent(Translations.Settings.ToggleRecompileExitPlayMode), HotReloadPrefs.AutoRecompileUnsupportedChangesOnExitPlayMode);
+ string toggleDescription;
+ if (HotReloadPrefs.AutoRecompileUnsupportedChangesOnExitPlayMode) {
+ toggleDescription = Translations.Settings.SettingsRecompileExitPlayModeOn;
+ } else {
+ toggleDescription = Translations.Settings.SettingsRecompileExitPlayModeOff;
+ }
+ EditorGUILayout.LabelField(toggleDescription, HotReloadWindowStyles.WrapStyle);
+ EditorGUILayout.EndToggleGroup();
+ }
+
+ void RenderOnDevice() {
+ HotReloadPrefs.ShowOnDevice = EditorGUILayout.Foldout(HotReloadPrefs.ShowOnDevice, Translations.Settings.SettingsOnDevice, true, HotReloadWindowStyles.FoldoutStyle);
+ if (!HotReloadPrefs.ShowOnDevice) {
+ return;
+ }
+ // header with explainer image
+ {
+ if (headlineStyle == null) {
+ // start with textArea for the background and border colors
+ headlineStyle = new GUIStyle(GUI.skin.label) {
+ fontStyle = FontStyle.Bold,
+ alignment = TextAnchor.MiddleLeft
+ };
+ headlineStyle.normal.textColor = HotReloadWindowStyles.H2TitleStyle.normal.textColor;
+
+ // bg color
+ if (HotReloadWindowStyles.IsDarkMode) {
+ headlineStyle.normal.background = EditorTextures.DarkGray40;
+ } else {
+ headlineStyle.normal.background = EditorTextures.LightGray225;
+ }
+ // layout
+ headlineStyle.padding = new RectOffset(8, 8, 0, 0);
+ headlineStyle.margin = new RectOffset(6, 6, 6, 6);
+ }
+ GUILayout.Space(9f); // space between logo and headline
+
+ GUILayout.Label(Translations.OnDevice.OnDeviceHeadline,
+ headlineStyle, GUILayout.MinHeight(EditorGUIUtility.singleLineHeight * 1.4f));
+ // image showing how Hot Reload works with a phone
+ // var bannerBox = GUILayoutUtility.GetRect(flowchart.width * 0.6f, flowchart.height * 0.6f);
+ // GUI.DrawTexture(bannerBox, flowchart, ScaleMode.ScaleToFit);
+ }
+
+ GUILayout.Space(16f);
+
+ //ButtonToOpenBuildSettings();
+
+ {
+ GUILayout.Label(Translations.Settings.SettingsManualConnect, HotReloadWindowStyles.H3TitleStyle);
+ EditorGUILayout.Space();
+
+ GUILayout.BeginHorizontal();
+
+ // indent all controls (this works with non-labels)
+ GUILayout.Space(16f);
+ GUILayout.BeginVertical();
+
+ string text;
+ var ip = IpHelper.GetIpAddressCached();
+ if (string.IsNullOrEmpty(ip)) {
+ text = string.Format(Translations.OnDevice.OnDeviceManualConnectFormat, RequestHelper.port);
+ } else {
+ text = string.Format(Translations.OnDevice.OnDeviceManualConnectWithIP, ip, RequestHelper.port);
+ }
+ GUILayout.Label(text, HotReloadWindowStyles.H3TitleWrapStyle);
+
+ if (!currentState.isServerHealthy) {
+ DrawHorizontalCheck(ServerHealthCheck.I.IsServerHealthy,
+ Translations.OnDevice.OnDeviceCheckHotReloadRunning,
+ Translations.OnDevice.OnDeviceCheckHotReloadNotRunning,
+ hasFix: false);
+ }
+
+ if (!HotReloadPrefs.ExposeServerToLocalNetwork) {
+ var summary = string.Format(Translations.OnDevice.OnDeviceCheckEnableExposeServer, new ExposeServerOption().ShortSummary);
+ DrawHorizontalCheck(HotReloadPrefs.ExposeServerToLocalNetwork,
+ summary,
+ summary);
+ }
+
+ // explainer image that shows phone needs same wifi to auto connect ?
+
+ GUILayout.EndVertical();
+ GUILayout.EndHorizontal();
+ }
+
+ GUILayout.Space(16f);
+
+ // loading again is smooth, pretty sure AssetDatabase.LoadAssetAtPath is caching -Troy
+ var settingsObject = HotReloadSettingsEditor.LoadSettingsOrDefault();
+ var so = new SerializedObject(settingsObject);
+
+ // if you build for Android now, will Hot Reload work?
+ {
+
+ EditorGUILayout.BeginHorizontal();
+ GUILayout.Label(Translations.Settings.SettingsBuildSettingsChecklist, HotReloadWindowStyles.H3TitleStyle);
+ EditorGUI.BeginDisabledGroup(isSupported);
+ // One-click to change each setting to the supported value
+ if (GUILayout.Button(Translations.Common.ButtonFixAll, GUILayout.MaxWidth(90f))) {
+ FixAllUnsupportedSettings(so);
+ }
+ EditorGUI.EndDisabledGroup();
+ EditorGUILayout.EndHorizontal();
+
+
+ // NOTE: After user changed some build settings, window may not immediately repaint
+ // (e.g. toggle Development Build in Build Settings window)
+ // We could show a refresh button (to encourage the user to click the window which makes it repaint).
+ DrawSectionCheckBuildSupport(so);
+ }
+
+
+ GUILayout.Space(16f);
+
+ // Settings checkboxes (Hot Reload options)
+ {
+ GUILayout.Label(Translations.Settings.SettingsOptions, HotReloadWindowStyles.H3TitleStyle);
+ if (settingsObject) {
+ optionsSection.DrawGUI(so);
+ }
+ }
+ GUILayout.FlexibleSpace(); // needed otherwise vertical scrollbar is appearing for no reason (Unity 2021 glitch perhaps)
+ }
+
+ private void RenderLicenseInfoSection() {
+ HotReloadRunTab.RenderLicenseInfo(
+ _window.RunTabState,
+ currentState.loginStatus,
+ verbose: true,
+ allowHide: false,
+ overrideActionButton:Translations.Common.ButtonActivateLicense,
+ showConsumptions: true
+ );
+ }
+
+ private void RenderPromoCodeSection() {
+ _window.RunTab.RenderPromoCodes();
+ }
+
+ public void FocusLicenseFoldout() {
+ HotReloadPrefs.ShowLogin = true;
+ }
+
+ // note: changing scripting backend does not force Unity to recreate the GUI, so need to check it when drawing.
+ private ScriptingImplementation ScriptingBackend => HotReloadBuildHelper.GetCurrentScriptingBackend();
+ private ManagedStrippingLevel StrippingLevel => HotReloadBuildHelper.GetCurrentStrippingLevel();
+ public bool isSupported = true;
+
+ ///
+ /// These options are drawn in the On-device tab
+ ///
+ // new on-device options should be added here
+ public static readonly IOption[] allOptions = new IOption[] {
+ new ExposeServerOption(),
+ IncludeInBuildOption.I,
+ new AllowAndroidAppToMakeHttpRequestsOption(),
+ };
+
+ ///
+ /// Change each setting to the value supported by Hot Reload
+ ///
+ private void FixAllUnsupportedSettings(SerializedObject so) {
+ if (!isCurrentBuildTargetSupported.Value) {
+ // try switch to Android platform
+ // (we also support Standalone but HotReload on mobile is a better selling point)
+ if (!TrySwitchToStandalone()) {
+ // skip changing other options (user won't readthe gray text) - user has to click Fix All again
+ return;
+ }
+ }
+
+ foreach (var buildOption in allOptions) {
+ if (!buildOption.GetValue(so)) {
+ buildOption.SetValue(so, true);
+ }
+ }
+ so.ApplyModifiedProperties();
+ var settingsObject = so.targetObject as HotReloadSettingsObject;
+ if (settingsObject) {
+ // when you click fix all, make sure to save the settings, otherwise ui does not update
+ HotReloadSettingsEditor.EnsureSettingsCreated(settingsObject);
+ }
+
+ if (!EditorUserBuildSettings.development) {
+ EditorUserBuildSettings.development = true;
+ }
+
+ HotReloadBuildHelper.SetCurrentScriptingBackend(ScriptingImplementation.Mono2x);
+ HotReloadBuildHelper.SetCurrentStrippingLevel(ManagedStrippingLevel.Disabled);
+ }
+
+ public static bool TrySwitchToStandalone() {
+ BuildTarget buildTarget;
+ if (Application.platform == RuntimePlatform.LinuxEditor) {
+ buildTarget = BuildTarget.StandaloneLinux64;
+ } else if (Application.platform == RuntimePlatform.WindowsEditor) {
+ buildTarget = BuildTarget.StandaloneWindows64;
+ } else if (Application.platform == RuntimePlatform.OSXEditor) {
+ buildTarget = BuildTarget.StandaloneOSX;
+ } else {
+ return false;
+ }
+ var current = EditorUserBuildSettings.activeBuildTarget;
+ if (current == buildTarget) {
+ return true;
+ }
+ var confirmed = EditorUtility.DisplayDialog(Translations.Dialogs.DialogTitleSwitchBuildTarget,
+ Translations.Dialogs.DialogMessageSwitchBuildTarget,
+ Translations.Dialogs.DialogButtonSwitchToStandalone, Translations.Common.ButtonCancel);
+ if (confirmed) {
+ EditorUserBuildSettings.SwitchActiveBuildTargetAsync(BuildTargetGroup.Standalone, buildTarget);
+ Log.Info(Translations.About.LogBuildTargetSwitching, buildTarget);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ ///
+ /// Section that user can check before making a Unity Player build.
+ ///
+ ///
+ ///
+ /// This section is for confirming your build will work with Hot Reload.
+ /// Options that can be changed after the build is made should be drawn elsewhere.
+ ///
+ public void DrawSectionCheckBuildSupport(SerializedObject so) {
+ isSupported = true;
+ var selectedPlatform = currentBuildTarget.Value;
+ DrawHorizontalCheck(isCurrentBuildTargetSupported.Value,
+ string.Format(Translations.OnDevice.OnDeviceCheckPlatformSelected, selectedPlatform.ToString()),
+ string.Format(Translations.OnDevice.OnDeviceCheckPlatformNotSupported, selectedPlatform.ToString()));
+
+ using (new EditorGUI.DisabledScope(!isCurrentBuildTargetSupported.Value)) {
+ foreach (var option in allOptions) {
+ DrawHorizontalCheck(option.GetValue(so),
+ string.Format(Translations.OnDevice.OnDeviceCheckEnableExposeServer, option.ShortSummary),
+ string.Format(Translations.OnDevice.OnDeviceCheckEnableExposeServer, option.ShortSummary));
+ }
+
+ DrawHorizontalCheck(EditorUserBuildSettings.development,
+ Translations.OnDevice.OnDeviceCheckDevelopmentEnabled,
+ Translations.OnDevice.OnDeviceCheckEnableDevelopment);
+
+ DrawHorizontalCheck(ScriptingBackend == ScriptingImplementation.Mono2x,
+ Translations.OnDevice.OnDeviceCheckMonoBackend,
+ Translations.OnDevice.OnDeviceCheckSetMonoBackend);
+
+ DrawHorizontalCheck(StrippingLevel == ManagedStrippingLevel.Disabled,
+ string.Format(Translations.OnDevice.OnDeviceCheckStrippingLevel, StrippingLevel),
+ string.Format(Translations.OnDevice.OnDeviceCheckStrippingLevel, StrippingLevel),
+ suggestedSolutionText: Translations.OnDevice.OnDeviceCheckStrippingSolution
+ );
+ }
+ }
+
+ ///
+ /// Draw a box with a tick or warning icon on the left, with text describing the tick or warning
+ ///
+ /// The condition to check. True to show a tick icon, False to show a warning.
+ /// Shown when condition is true
+ /// Shown when condition is false
+ /// Shown when is false
+ void DrawHorizontalCheck(bool condition, string okText, string notOkText = null, string suggestedSolutionText = null, bool hasFix = true) {
+ if (okText == null) {
+ throw new ArgumentNullException(nameof(okText));
+ }
+ if (notOkText == null) {
+ notOkText = okText;
+ }
+
+ // include some horizontal space around the icon
+ var boxWidth = GUILayout.Width(EditorGUIUtility.singleLineHeight * 1.31f);
+ var height = GUILayout.Height(EditorGUIUtility.singleLineHeight * 1.01f);
+ GUILayout.BeginHorizontal(HotReloadWindowStyles.BoxStyle, height, GUILayout.ExpandWidth(true));
+ var style = HotReloadWindowStyles.NoPaddingMiddleLeftStyle;
+ var iconRect = GUILayoutUtility.GetRect(
+ Mathf.Round(EditorGUIUtility.singleLineHeight * 1.31f),
+ Mathf.Round(EditorGUIUtility.singleLineHeight * 1.01f),
+ style, boxWidth, height, GUILayout.ExpandWidth(false));
+ // rounded so we can have pixel perfect black circle bg
+ iconRect.Set(Mathf.Round(iconRect.x), Mathf.Round(iconRect.y), Mathf.CeilToInt(iconRect.width),
+ Mathf.CeilToInt(iconRect.height));
+ var text = condition ? okText : notOkText;
+ var icon = condition ? iconCheck : iconWarning;
+ if (GUI.enabled) {
+ DrawBlackCircle(iconRect);
+ // resource can be null when building player (Editor Resources not available)
+ if (icon) {
+ GUI.DrawTexture(iconRect, icon, ScaleMode.ScaleToFit);
+ }
+ } else {
+ // show something (instead of hiding) so that layout stays same size
+ DrawDisabledCircle(iconRect);
+ }
+ GUILayout.Space(4f);
+ GUILayout.Label(text, style, height);
+
+ if (!condition && hasFix) {
+ isSupported = false;
+ }
+
+ GUILayout.EndHorizontal();
+ if (!condition && !String.IsNullOrEmpty(suggestedSolutionText)) {
+ // suggest to the user how they can resolve the issue
+ EditorGUI.indentLevel++;
+ GUILayout.Label(suggestedSolutionText, HotReloadWindowStyles.WrapStyle);
+ EditorGUI.indentLevel--;
+ }
+ }
+
+ void DrawDisabledCircle(Rect rect) => DrawCircleIcon(rect,
+ Resources.Load("icon_circle_gray"),
+ Color.clear); // smaller circle draws less attention
+
+ void DrawBlackCircle(Rect rect) => DrawCircleIcon(rect,
+ Resources.Load("icon_circle_black"),
+ new Color(0.14f, 0.14f, 0.14f)); // black is too dark in unity light theme
+
+ void DrawCircleIcon(Rect rect, Texture circleIcon, Color borderColor) {
+ // Note: drawing texture from resources is pixelated on the edges, so it has some transperancy around the edges.
+ // While building for Android, Resources.Load returns null for our editor Resources.
+ if (circleIcon != null) {
+ GUI.DrawTexture(rect, circleIcon, ScaleMode.ScaleToFit);
+ }
+
+ // Draw smooth circle border
+ const float borderWidth = 2f;
+ GUI.DrawTexture(rect, EditorTextures.White, ScaleMode.ScaleToFit, true,
+ 0f,
+ borderColor,
+ new Vector4(borderWidth, borderWidth, borderWidth, borderWidth),
+ Mathf.Min(rect.height, rect.width) / 2f);
+ }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadSettingsTab.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadSettingsTab.cs.meta
new file mode 100644
index 0000000..00d31cd
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadSettingsTab.cs.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: fff71bd159424bf2978e2e99eacba9b4
+timeCreated: 1674057842
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/GUI/Tabs/HotReloadSettingsTab.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/HotReloadWindow.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/HotReloadWindow.cs
new file mode 100644
index 0000000..8e782bb
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/HotReloadWindow.cs
@@ -0,0 +1,393 @@
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using System.Threading;
+using SingularityGroup.HotReload.DTO;
+using SingularityGroup.HotReload.Editor.Cli;
+using SingularityGroup.HotReload.Editor.Semver;
+using UnityEditor;
+using UnityEditor.Compilation;
+using SingularityGroup.HotReload.Editor.Localization;
+using UnityEngine;
+
+[assembly: InternalsVisibleTo("SingularityGroup.HotReload.EditorSamples")]
+
+namespace SingularityGroup.HotReload.Editor {
+ class HotReloadWindow : EditorWindow {
+ public static HotReloadWindow Current { get; private set; }
+
+ List tabs;
+ List Tabs => tabs ?? (tabs = new List {
+ RunTab,
+ SettingsTab,
+ AboutTab,
+ });
+ int selectedTab;
+
+ internal static Vector2 scrollPos;
+
+ static Timer timer;
+
+
+ HotReloadRunTab runTab;
+ internal HotReloadRunTab RunTab => runTab ?? (runTab = new HotReloadRunTab(this));
+ HotReloadSettingsTab settingsTab;
+ internal HotReloadSettingsTab SettingsTab => settingsTab ?? (settingsTab = new HotReloadSettingsTab(this));
+ HotReloadAboutTab aboutTab;
+ internal HotReloadAboutTab AboutTab => aboutTab ?? (aboutTab = new HotReloadAboutTab(this));
+
+ static ShowOnStartupEnum _showOnStartupOption;
+
+ ///
+ /// This token is cancelled when the EditorWindow is disabled.
+ ///
+ ///
+ /// Use it for all tasks.
+ /// When token is cancelled, scripts are about to be recompiled and this will cause tasks to fail for weird reasons.
+ ///
+ public CancellationToken cancelToken;
+ CancellationTokenSource cancelTokenSource;
+
+ static readonly PackageUpdateChecker packageUpdateChecker = new PackageUpdateChecker();
+
+ [MenuItem(Translations.MenuItems.OpenHotReload)]
+ internal static void Open() {
+ // Don't open Hot Reload window inside Virtual Player folder
+ if (MultiplayerPlaymodeHelper.IsClone) {
+ Log.Info("Virtual Player instances use the same Hot Reload server instance as the Main Editor. Use Hot Reload window in the Main Editor.");
+ return;
+ }
+ // opening the window on CI systems was keeping Unity open indefinitely
+ if (EditorWindowHelper.IsHumanControllingUs()) {
+ if (Current) {
+ Current.Show();
+ Current.Focus();
+ } else {
+ Current = GetWindow();
+ }
+ }
+ }
+
+ [MenuItem(Translations.MenuItems.RecompileHotReload)]
+ internal static void Recompile() {
+ HotReloadRunTab.Recompile();
+ }
+
+ void OnInterval(object o) {
+ HotReloadRunTab.RepaintInstant();
+ }
+
+ void OnEnable() {
+ if (timer == null) {
+ timer = new Timer(OnInterval, null, 20 * 1000, 20 * 1000);
+ }
+ Current = this;
+ if (cancelTokenSource != null) {
+ cancelTokenSource.Cancel();
+ }
+ // Set min size initially so that full UI is visible
+ if (!HotReloadPrefs.OpenedWindowAtLeastOnce) {
+ this.minSize = new Vector2(Constants.RecompileButtonTextHideWidth + 1, Constants.EventsListHideHeight + 70);
+ HotReloadPrefs.OpenedWindowAtLeastOnce = true;
+ }
+ cancelTokenSource = new CancellationTokenSource();
+ cancelToken = cancelTokenSource.Token;
+
+ this.titleContent = new GUIContent(" Hot Reload", GUIHelper.GetInvertibleIcon(InvertibleIcon.Logo));
+ _showOnStartupOption = HotReloadPrefs.ShowOnStartup;
+
+ packageUpdateChecker.StartCheckingForNewVersion();
+ }
+
+ void Update() {
+ foreach (var tab in Tabs) {
+ tab.Update();
+ }
+ }
+
+ void OnDisable() {
+ if (cancelTokenSource != null) {
+ cancelTokenSource.Cancel();
+ cancelTokenSource = null;
+ }
+
+ if (Current == this) {
+ Current = null;
+ }
+ timer.Dispose();
+ timer = null;
+ }
+
+ internal void SelectTab(Type tabType) {
+ selectedTab = Tabs.FindIndex(x => x.GetType() == tabType);
+ }
+
+ public HotReloadRunTabState RunTabState { get; private set; }
+ void OnGUI() {
+ // TabState ensures rendering is consistent between Layout and Repaint calls
+ // Without it errors like this happen:
+ // ArgumentException: Getting control 2's position in a group with only 2 controls when doing repaint
+ // See thread for more context: https://answers.unity.com/questions/17718/argumentexception-getting-control-2s-position-in-a.html
+ if (Event.current.type == EventType.Layout) {
+ RunTabState = HotReloadRunTabState.Current;
+ }
+ using(var scope = new EditorGUILayout.ScrollViewScope(scrollPos, false, false)) {
+ scrollPos = scope.scrollPosition;
+ // RenderDebug();
+ RenderTabs();
+ }
+ GUILayout.FlexibleSpace(); // GUI below will be rendered on the bottom
+ if (HotReloadWindowStyles.windowScreenHeight > 90)
+ RenderBottomBar();
+ }
+
+ void RenderDebug() {
+ if (GUILayout.Button("RESET WINDOW")) {
+ OnDisable();
+
+ RequestHelper.RequestLogin("test", "test", 1).Forget();
+
+ HotReloadPrefs.LicenseEmail = null;
+ HotReloadPrefs.ExposeServerToLocalNetwork = true;
+ HotReloadPrefs.LicensePassword = null;
+ HotReloadPrefs.LoggedBurstHint = false;
+ HotReloadPrefs.DontShowPromptForDownload = false;
+ HotReloadPrefs.RateAppShown = false;
+ HotReloadPrefs.ActiveDays = string.Empty;
+ HotReloadPrefs.LaunchOnEditorStart = false;
+ HotReloadPrefs.ShowUnsupportedChanges = true;
+ HotReloadPrefs.RedeemLicenseEmail = null;
+ HotReloadPrefs.RedeemLicenseInvoice = null;
+ OnEnable();
+ File.Delete(EditorCodePatcher.serverDownloader.GetExecutablePath(HotReloadCli.controller));
+ InstallUtility.DebugClearInstallState();
+ InstallUtility.CheckForNewInstall();
+ EditorPrefs.DeleteKey(Attribution.LastLoginKey);
+ File.Delete(RedeemLicenseHelper.registerOutcomePath);
+
+ CompileMethodDetourer.Reset();
+ AssetDatabase.Refresh();
+ }
+ }
+
+ internal static void RenderLogo(int width = 243) {
+ var isDarkMode = HotReloadWindowStyles.IsDarkMode;
+ var tex = Resources.Load(isDarkMode ? "Logo_HotReload_DarkMode" : "Logo_HotReload_LightMode");
+ //Can happen during player builds where Editor Resources are unavailable
+ if(tex == null) {
+ return;
+ }
+ var targetWidth = width;
+ var targetHeight = 44;
+ GUILayout.Space(4f);
+ // background padding top and bottom
+ float padding = 5f;
+ // reserve layout space for the texture
+ var backgroundRect = GUILayoutUtility.GetRect(targetWidth + padding, targetHeight + padding, HotReloadWindowStyles.LogoStyle);
+ // draw the texture into that reserved space. First the bg then the logo.
+ if (isDarkMode) {
+ GUI.DrawTexture(backgroundRect, EditorTextures.DarkGray17, ScaleMode.StretchToFill);
+ } else {
+ GUI.DrawTexture(backgroundRect, EditorTextures.LightGray238, ScaleMode.StretchToFill);
+ }
+
+ var foregroundRect = backgroundRect;
+ foregroundRect.yMin += padding;
+ foregroundRect.yMax -= padding;
+ // during player build (EditorWindow still visible), Resources.Load returns null
+ if (tex) {
+ GUI.DrawTexture(foregroundRect, tex, ScaleMode.ScaleToFit);
+ }
+ }
+
+ int? collapsedTab;
+ void RenderTabs() {
+ using(new EditorGUILayout.VerticalScope(HotReloadWindowStyles.BoxStyle)) {
+ if (HotReloadWindowStyles.windowScreenHeight > 210 && HotReloadWindowStyles.windowScreenWidth > 375) {
+ selectedTab = GUILayout.Toolbar(
+ selectedTab,
+ Tabs.Select(t =>
+ new GUIContent(t.Title.StartsWith(" ", StringComparison.Ordinal) ? t.Title : " " + t.Title,
+ t.Icon, t.Tooltip)).ToArray(),
+ GUILayout.Height(22f) // required, otherwise largest icon height determines toolbar height
+ );
+ if (collapsedTab != null) {
+ selectedTab = collapsedTab.Value;
+ collapsedTab = null;
+ }
+ } else {
+ if (collapsedTab == null) {
+ collapsedTab = selectedTab;
+ }
+ // When window is super small, we pretty much can only show run tab
+ SelectTab(typeof(HotReloadRunTab));
+ }
+
+ if (HotReloadWindowStyles.windowScreenHeight > 250 && HotReloadWindowStyles.windowScreenWidth > 275) {
+ RenderLogo();
+ }
+
+ Tabs[selectedTab].OnGUI();
+ }
+ }
+
+ void RenderBottomBar() {
+ SemVersion newVersion;
+ var updateAvailable = packageUpdateChecker.TryGetNewVersion(out newVersion);
+
+ if (HotReloadWindowStyles.windowScreenWidth > Constants.RateAppHideWidth
+ && HotReloadWindowStyles.windowScreenHeight > Constants.RateAppHideHeight
+ ) {
+ RenderRateApp();
+ }
+
+ if (updateAvailable) {
+ RenderUpdateButton(newVersion);
+ }
+
+ using(new EditorGUILayout.HorizontalScope("ProjectBrowserBottomBarBg", GUILayout.ExpandWidth(true), GUILayout.Height(25f))) {
+ RenderBottomBarCore();
+ }
+ }
+
+ static GUIStyle _renderAppBoxStyle;
+ static GUIStyle renderAppBoxStyle => _renderAppBoxStyle ?? (_renderAppBoxStyle = new GUIStyle(GUI.skin.box) {
+ padding = new RectOffset(10, 10, 0, 0)
+ });
+
+ static GUILayoutOption[] _nonExpandable;
+ public static GUILayoutOption[] NonExpandableLayout => _nonExpandable ?? (_nonExpandable = new [] {GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(true)});
+
+ internal static void RenderRateApp() {
+ if (!ShouldShowRateApp()) {
+ return;
+ }
+ using (new EditorGUILayout.VerticalScope(renderAppBoxStyle)) {
+ using (new EditorGUILayout.HorizontalScope()) {
+ HotReloadGUIHelper.HelpBox(Translations.Miscellaneous.RateAppQuestion, MessageType.Info, 11);
+ if (GUILayout.Button(Translations.Common.ButtonHide, NonExpandableLayout)) {
+ RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.RateApp), new EditorExtraData { { "dismissed", true } }).Forget();
+ HotReloadPrefs.RateAppShown = true;
+ }
+ }
+ using (new EditorGUILayout.HorizontalScope()) {
+ if (GUILayout.Button(Translations.Common.ButtonYes)) {
+ var openedUrl = PackageConst.IsAssetStoreBuild && EditorUtility.DisplayDialog(Translations.Dialogs.DialogTitleRateApp, Translations.Dialogs.DialogMessageRateApp, Translations.Common.ButtonOpenInBrowser, Translations.Common.ButtonCancel);
+ if (openedUrl) {
+ Application.OpenURL(Constants.UnityStoreRateAppURL);
+ }
+ HotReloadPrefs.RateAppShown = true;
+ var data = new EditorExtraData();
+ if (PackageConst.IsAssetStoreBuild) {
+ data.Add("opened_url", openedUrl);
+ }
+ data.Add("enjoy_app", true);
+ RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.RateApp), data).Forget();
+ }
+ if (GUILayout.Button(Translations.Common.ButtonNo)) {
+ HotReloadPrefs.RateAppShown = true;
+ var data = new EditorExtraData();
+ data.Add("enjoy_app", false);
+ RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.RateApp), data).Forget();
+ }
+ }
+ }
+ }
+
+ internal static bool ShouldShowRateApp() {
+ if (HotReloadPrefs.RateAppShown) {
+ return false;
+ }
+ var activeDays = EditorCodePatcher.GetActiveDaysForRateApp();
+ if (activeDays.Count < Constants.DaysToRateApp) {
+ return false;
+ }
+ return true;
+ }
+
+ void RenderUpdateButton(SemVersion newVersion) {
+ if (GUILayout.Button(string.Format(Translations.Miscellaneous.ButtonUpdateToVersionFormat, newVersion), HotReloadWindowStyles.UpgradeButtonStyle)) {
+ packageUpdateChecker.UpdatePackageAsync(newVersion).Forget(CancellationToken.None);
+ }
+ }
+
+ internal static void RenderShowOnStartup() {
+ var prevLabelWidth = EditorGUIUtility.labelWidth;
+ try {
+ EditorGUIUtility.labelWidth = 105f;
+ using (new GUILayout.VerticalScope()) {
+ using (new GUILayout.HorizontalScope()) {
+ GUILayout.Label(Translations.Common.LabelShowOnStartup);
+ Rect buttonRect = GUILayoutUtility.GetLastRect();
+ if (EditorGUILayout.DropdownButton(new GUIContent(Regex.Replace(_showOnStartupOption.ToString(), "([a-z])([A-Z])", "$1 $2")), FocusType.Passive, GUILayout.Width(110f))) {
+ GenericMenu menu = new GenericMenu();
+ foreach (ShowOnStartupEnum option in Enum.GetValues(typeof(ShowOnStartupEnum))) {
+ menu.AddItem(new GUIContent(Regex.Replace(option.ToString(), "([a-z])([A-Z])", "$1 $2")), false, () => {
+ if (_showOnStartupOption != option) {
+ _showOnStartupOption = option;
+ HotReloadPrefs.ShowOnStartup = _showOnStartupOption;
+ }
+ });
+ }
+ menu.DropDown(new Rect(buttonRect.x, buttonRect.y, 100, 0));
+ }
+ }
+ }
+ } finally {
+ EditorGUIUtility.labelWidth = prevLabelWidth;
+ }
+ }
+
+ void RenderBottomBarCore() {
+ bool troubleshootingShown = EditorCodePatcher.Started && HotReloadWindowStyles.windowScreenWidth >= 400;
+ bool alertsShown = EditorCodePatcher.Started && HotReloadWindowStyles.windowScreenWidth > Constants.EventFiltersShownHideWidth;
+ using (new EditorGUILayout.VerticalScope()) {
+ using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.FooterStyle)) {
+ if (!troubleshootingShown) {
+ GUILayout.FlexibleSpace();
+ if (alertsShown) {
+ GUILayout.Space(-20);
+ }
+ } else {
+ GUILayout.Space(21);
+ }
+ GUILayout.Space(0);
+ var lastRect = GUILayoutUtility.GetLastRect();
+ // show events button when scrolls are hidden
+ if (!HotReloadRunTab.CanRenderBars(RunTabState) && !RunTabState.starting) {
+ using (new EditorGUILayout.VerticalScope()) {
+ GUILayout.FlexibleSpace();
+ var icon = HotReloadState.ShowingRedDot ? InvertibleIcon.EventsNew : InvertibleIcon.Events;
+ if (GUILayout.Button(new GUIContent("", GUIHelper.GetInvertibleIcon(icon)))) {
+ PopupWindow.Show(new Rect(lastRect.x, lastRect.y, 0, 0), HotReloadEventPopup.I);
+ }
+ GUILayout.FlexibleSpace();
+ }
+ GUILayout.Space(3f);
+ }
+ if (alertsShown) {
+ using (new EditorGUILayout.VerticalScope()) {
+ GUILayout.FlexibleSpace();
+ HotReloadTimelineHelper.RenderAlertFilters();
+ GUILayout.FlexibleSpace();
+ }
+ }
+
+ GUILayout.FlexibleSpace();
+ if (troubleshootingShown) {
+ using (new EditorGUILayout.VerticalScope()) {
+ GUILayout.FlexibleSpace();
+ OpenURLButton.Render(Translations.Miscellaneous.ButtonTroubleshooting, Constants.TroubleshootingURL);
+ GUILayout.FlexibleSpace();
+ }
+ GUILayout.Space(21);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/HotReloadWindow.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/HotReloadWindow.cs.meta
new file mode 100644
index 0000000..c9d5bf0
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/HotReloadWindow.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: f62a84c0b148b0a4582bdd9f1a69e6d3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/HotReloadWindow.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/ShowOnStartupEnum.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/ShowOnStartupEnum.cs
new file mode 100644
index 0000000..17f2f50
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/ShowOnStartupEnum.cs
@@ -0,0 +1,7 @@
+namespace SingularityGroup.HotReload.Editor {
+ enum ShowOnStartupEnum {
+ Always,
+ OnNewVersion,
+ Never,
+ }
+}
\ No newline at end of file
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/ShowOnStartupEnum.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/ShowOnStartupEnum.cs.meta
new file mode 100644
index 0000000..5038590
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/ShowOnStartupEnum.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: 809f47245f717ad41996974be2443feb
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/ShowOnStartupEnum.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/Styles.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/Styles.meta
new file mode 100644
index 0000000..48002b6
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/Styles.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 83e25ceea0bb7cd4ebf04b724bb0584c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/Styles/HotReloadWindowStyles.cs b/Packages/com.singularitygroup.hotreload/Editor/Window/Styles/HotReloadWindowStyles.cs
new file mode 100644
index 0000000..4f4ede9
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/Styles/HotReloadWindowStyles.cs
@@ -0,0 +1,777 @@
+using UnityEditor;
+using UnityEngine;
+using System.Reflection;
+
+namespace SingularityGroup.HotReload.Editor {
+ internal static class HotReloadWindowStyles {
+ private static GUIStyle h1TitleStyle;
+ private static GUIStyle h1TitleCenteredStyle;
+ private static GUIStyle h2TitleStyle;
+ private static GUIStyle h3TitleStyle;
+ private static GUIStyle h3TitleWrapStyle;
+ private static GUIStyle h4TitleStyle;
+ private static GUIStyle h5TitleStyle;
+ private static GUIStyle boxStyle;
+ private static GUIStyle wrapStyle;
+ private static GUIStyle noPaddingMiddleLeftStyle;
+ private static GUIStyle middleLeftStyle;
+ private static GUIStyle middleCenterStyle;
+ private static GUIStyle mediumMiddleCenterStyle;
+ private static GUIStyle textFieldWrapStyle;
+ private static GUIStyle foldoutStyle;
+ private static GUIStyle h3CenterTitleStyle;
+ private static GUIStyle logoStyle;
+ private static GUIStyle changelogPointersStyle;
+ private static GUIStyle recompileButtonStyle;
+ private static GUIStyle indicationIconStyle;
+ private static GUIStyle indicationAlertIconStyle;
+ private static GUIStyle startButtonStyle;
+ private static GUIStyle stopButtonStyle;
+ private static GUIStyle eventFilters;
+ private static GUIStyle sectionOuterBoxCompactStyle;
+ private static GUIStyle sectionInnerBoxStyle;
+ private static GUIStyle sectionInnerBoxWideStyle;
+ private static GUIStyle changelogSectionInnerBoxStyle;
+ private static GUIStyle indicationBoxStyle;
+ private static GUIStyle linkStyle;
+ private static GUIStyle labelStyle;
+ private static GUIStyle progressBarBarStyle;
+ private static GUIStyle section;
+ private static GUIStyle scroll;
+ private static GUIStyle barStyle;
+ private static GUIStyle barBgStyle;
+ private static GUIStyle barChildStyle;
+ private static GUIStyle barFoldoutStyle;
+ private static GUIStyle timestampStyle;
+ private static GUIStyle clickableLabelBoldStyle;
+ private static GUIStyle _footerStyle;
+ private static GUIStyle _emptyListText;
+ private static GUIStyle _stacktraceTextAreaStyle;
+ private static GUIStyle _customFoldoutStyle;
+ private static GUIStyle _entryBoxStyle;
+ private static GUIStyle _childEntryBoxStyle;
+ private static GUIStyle _removeIconStyle;
+ private static GUIStyle upgradeLicenseButtonStyle;
+ private static GUIStyle upgradeLicenseButtonOverlayStyle;
+ private static GUIStyle upgradeButtonStyle;
+ private static GUIStyle hideButtonStyle;
+ private static GUIStyle dynamicSection;
+ private static GUIStyle dynamicSectionHelpTab;
+ private static GUIStyle helpTabButton;
+ private static GUIStyle indicationHelpBox;
+ private static GUIStyle notificationsTitleStyle;
+
+ private static Color32? darkModeLinkColor;
+ private static Color32? lightModeModeLinkColor;
+
+ public static bool IsDarkMode => EditorGUIUtility.isProSkin;
+ public static int windowScreenWidth => HotReloadWindow.Current ? (int)HotReloadWindow.Current.position.width : Screen.width;
+ public static int windowScreenHeight => HotReloadWindow.Current ? (int)HotReloadWindow.Current.position.height : Screen.height;
+ public static GUIStyle H1TitleStyle {
+ get {
+ if (h1TitleStyle == null) {
+ h1TitleStyle = new GUIStyle(EditorStyles.label);
+ h1TitleStyle.normal.textColor = EditorStyles.label.normal.textColor;
+ h1TitleStyle.fontStyle = FontStyle.Bold;
+ h1TitleStyle.fontSize = 16;
+ h1TitleStyle.padding.top = 5;
+ h1TitleStyle.padding.bottom = 5;
+ }
+ return h1TitleStyle;
+ }
+ }
+
+ public static GUIStyle FooterStyle {
+ get {
+ if (_footerStyle == null) {
+ _footerStyle = new GUIStyle();
+ _footerStyle.fixedHeight = 28;
+ }
+ return _footerStyle;
+ }
+ }
+
+ public static GUIStyle H1TitleCenteredStyle {
+ get {
+ if (h1TitleCenteredStyle == null) {
+ h1TitleCenteredStyle = new GUIStyle(H1TitleStyle);
+ h1TitleCenteredStyle.alignment = TextAnchor.MiddleCenter;
+ }
+ return h1TitleCenteredStyle;
+ }
+ }
+
+ public static GUIStyle H2TitleStyle {
+ get {
+ if (h2TitleStyle == null) {
+ h2TitleStyle = new GUIStyle(EditorStyles.label);
+ h2TitleStyle.normal.textColor = EditorStyles.label.normal.textColor;
+ h2TitleStyle.fontStyle = FontStyle.Bold;
+ h2TitleStyle.fontSize = 14;
+ h2TitleStyle.padding.top = 5;
+ h2TitleStyle.padding.bottom = 5;
+ }
+ return h2TitleStyle;
+ }
+ }
+
+ public static GUIStyle H3TitleStyle {
+ get {
+ if (h3TitleStyle == null) {
+ h3TitleStyle = new GUIStyle(EditorStyles.label);
+ h3TitleStyle.normal.textColor = EditorStyles.label.normal.textColor;
+ h3TitleStyle.fontStyle = FontStyle.Bold;
+ h3TitleStyle.fontSize = 12;
+ h3TitleStyle.padding.top = 5;
+ h3TitleStyle.padding.bottom = 5;
+ }
+ return h3TitleStyle;
+ }
+ }
+
+ public static GUIStyle NotificationsTitleStyle {
+ get {
+ if (notificationsTitleStyle == null) {
+ notificationsTitleStyle = new GUIStyle(HotReloadWindowStyles.H3TitleStyle);
+ notificationsTitleStyle.padding.bottom = 0;
+ notificationsTitleStyle.padding.top = 0;
+ }
+ return notificationsTitleStyle;
+ }
+ }
+
+ public static GUIStyle H3TitleWrapStyle {
+ get {
+ if (h3TitleWrapStyle == null) {
+ h3TitleWrapStyle = new GUIStyle(H3TitleStyle);
+ h3TitleWrapStyle.wordWrap = true;
+ }
+ return h3TitleWrapStyle;
+ }
+ }
+
+ public static GUIStyle H3CenteredTitleStyle {
+ get {
+ if (h3CenterTitleStyle == null) {
+ h3CenterTitleStyle = new GUIStyle(EditorStyles.label);
+ h3CenterTitleStyle.normal.textColor = EditorStyles.label.normal.textColor;
+ h3CenterTitleStyle.fontStyle = FontStyle.Bold;
+ h3CenterTitleStyle.alignment = TextAnchor.MiddleCenter;
+ h3CenterTitleStyle.fontSize = 12;
+ }
+ return h3CenterTitleStyle;
+ }
+ }
+
+ public static GUIStyle H4TitleStyle {
+ get {
+ if (h4TitleStyle == null) {
+ h4TitleStyle = new GUIStyle(EditorStyles.label);
+ h4TitleStyle.normal.textColor = EditorStyles.label.normal.textColor;
+ h4TitleStyle.fontStyle = FontStyle.Bold;
+ h4TitleStyle.fontSize = 11;
+ }
+ return h4TitleStyle;
+ }
+ }
+
+ public static GUIStyle H5TitleStyle {
+ get {
+ if (h5TitleStyle == null) {
+ h5TitleStyle = new GUIStyle(EditorStyles.label);
+ h5TitleStyle.normal.textColor = EditorStyles.label.normal.textColor;
+ h5TitleStyle.fontStyle = FontStyle.Bold;
+ h5TitleStyle.fontSize = 10;
+ }
+ return h5TitleStyle;
+ }
+ }
+
+ public static GUIStyle LabelStyle {
+ get {
+ if (labelStyle == null) {
+ labelStyle = new GUIStyle(EditorStyles.label);
+ labelStyle.fontSize = 12;
+ labelStyle.clipping = TextClipping.Clip;
+ labelStyle.wordWrap = true;
+ }
+ return labelStyle;
+ }
+ }
+
+ public static GUIStyle BoxStyle {
+ get {
+ if (boxStyle == null) {
+ boxStyle = new GUIStyle(EditorStyles.helpBox);
+ boxStyle.normal.textColor = GUI.skin.label.normal.textColor;
+ boxStyle.fontStyle = FontStyle.Bold;
+ boxStyle.alignment = TextAnchor.UpperLeft;
+ }
+ if (!IsDarkMode) {
+ boxStyle.normal.background = Texture2D.blackTexture;
+ }
+ return boxStyle;
+ }
+ }
+
+ public static GUIStyle WrapStyle {
+ get {
+ if (wrapStyle == null) {
+ wrapStyle = new GUIStyle(EditorStyles.label);
+ wrapStyle.fontStyle = FontStyle.Normal;
+ wrapStyle.wordWrap = true;
+ }
+ return wrapStyle;
+ }
+ }
+
+ public static GUIStyle NoPaddingMiddleLeftStyle {
+ get {
+ if (noPaddingMiddleLeftStyle == null) {
+ noPaddingMiddleLeftStyle = new GUIStyle(EditorStyles.label);
+ noPaddingMiddleLeftStyle.normal.textColor = GUI.skin.label.normal.textColor;
+ noPaddingMiddleLeftStyle.padding = new RectOffset();
+ noPaddingMiddleLeftStyle.margin = new RectOffset();
+ noPaddingMiddleLeftStyle.alignment = TextAnchor.MiddleLeft;
+ }
+ return noPaddingMiddleLeftStyle;
+ }
+ }
+
+ public static GUIStyle MiddleLeftStyle {
+ get {
+ if (middleLeftStyle == null) {
+ middleLeftStyle = new GUIStyle(EditorStyles.label);
+ middleLeftStyle.fontStyle = FontStyle.Normal;
+ middleLeftStyle.alignment = TextAnchor.MiddleLeft;
+ }
+
+ return middleLeftStyle;
+ }
+ }
+
+ public static GUIStyle MiddleCenterStyle {
+ get {
+ if (middleCenterStyle == null) {
+ middleCenterStyle = new GUIStyle(EditorStyles.label);
+ middleCenterStyle.fontStyle = FontStyle.Normal;
+ middleCenterStyle.alignment = TextAnchor.MiddleCenter;
+ }
+ return middleCenterStyle;
+ }
+ }
+
+ public static GUIStyle MediumMiddleCenterStyle {
+ get {
+ if (mediumMiddleCenterStyle == null) {
+ mediumMiddleCenterStyle = new GUIStyle(EditorStyles.label);
+ mediumMiddleCenterStyle.fontStyle = FontStyle.Normal;
+ mediumMiddleCenterStyle.fontSize = 12;
+ mediumMiddleCenterStyle.alignment = TextAnchor.MiddleCenter;
+ }
+ return mediumMiddleCenterStyle;
+ }
+ }
+
+ public static GUIStyle TextFieldWrapStyle {
+ get {
+ if (textFieldWrapStyle == null) {
+ textFieldWrapStyle = new GUIStyle(EditorStyles.textField);
+ textFieldWrapStyle.wordWrap = true;
+ }
+ return textFieldWrapStyle;
+ }
+ }
+
+ public static GUIStyle FoldoutStyle {
+ get {
+ if (foldoutStyle == null) {
+ foldoutStyle = new GUIStyle(EditorStyles.foldout);
+ foldoutStyle.normal.textColor = GUI.skin.label.normal.textColor;
+ foldoutStyle.alignment = TextAnchor.MiddleLeft;
+ foldoutStyle.fontStyle = FontStyle.Bold;
+ foldoutStyle.fontSize = 12;
+ }
+ return foldoutStyle;
+ }
+ }
+
+ public static GUIStyle LogoStyle {
+ get {
+ if (logoStyle == null) {
+ logoStyle = new GUIStyle();
+ logoStyle.margin = new RectOffset(6, 6, 0, 0);
+ logoStyle.padding = new RectOffset(16, 16, 0, 0);
+ }
+ return logoStyle;
+ }
+ }
+
+ public static GUIStyle ChangelogPointerStyle {
+ get {
+ if (changelogPointersStyle == null) {
+ changelogPointersStyle = new GUIStyle(EditorStyles.label);
+ changelogPointersStyle.wordWrap = true;
+ changelogPointersStyle.fontSize = 12;
+ changelogPointersStyle.padding.left = 20;
+ }
+ return changelogPointersStyle;
+ }
+ }
+
+ public static GUIStyle IndicationIcon {
+ get {
+ if (indicationIconStyle == null) {
+ indicationIconStyle = new GUIStyle(H2TitleStyle);
+ indicationIconStyle.fixedHeight = 20;
+ }
+ indicationIconStyle.padding = new RectOffset(left: windowScreenWidth > Constants.IndicationTextHideWidth ? 7 : 5, right: windowScreenWidth > Constants.IndicationTextHideWidth ? 0 : -10, top: 1, bottom: 1);
+ return indicationIconStyle;
+ }
+ }
+
+ public static GUIStyle IndicationAlertIcon {
+ get {
+ if (indicationAlertIconStyle == null) {
+ indicationAlertIconStyle = new GUIStyle(H2TitleStyle);
+ indicationAlertIconStyle.padding = new RectOffset(left: 5, right: -7, top: 1, bottom: 1);
+ indicationAlertIconStyle.fixedHeight = 20;
+ }
+ return indicationAlertIconStyle;
+ }
+ }
+
+ public static GUIStyle RecompileButton {
+ get {
+ if (recompileButtonStyle == null) {
+ recompileButtonStyle = new GUIStyle(EditorStyles.miniButton);
+ recompileButtonStyle.margin.top = 17;
+ recompileButtonStyle.fixedHeight = 25;
+ recompileButtonStyle.margin.right = 5;
+ }
+ recompileButtonStyle.fixedWidth = windowScreenWidth > Constants.RecompileButtonTextHideWidth ? 95 : 30;
+ return recompileButtonStyle;
+ }
+ }
+
+ public static GUIStyle StartButton {
+ get {
+ if (startButtonStyle == null) {
+ startButtonStyle = new GUIStyle(EditorStyles.miniButton);
+ startButtonStyle.fixedHeight = 25;
+ startButtonStyle.padding.top = 6;
+ startButtonStyle.padding.bottom = 6;
+ startButtonStyle.margin.top = 17;
+ }
+ startButtonStyle.fixedWidth = windowScreenWidth > Constants.StartButtonTextHideWidth ? 70 : 30;
+ return startButtonStyle;
+ }
+ }
+
+ public static GUIStyle StopButton {
+ get {
+ if (stopButtonStyle == null) {
+ stopButtonStyle = new GUIStyle(EditorStyles.miniButton);
+ stopButtonStyle.fixedHeight = 25;
+ stopButtonStyle.margin.top = 17;
+ }
+ stopButtonStyle.fixedWidth = HotReloadWindowStyles.windowScreenWidth > Constants.StartButtonTextHideWidth ? 70 : 30;
+ return stopButtonStyle;
+ }
+ }
+
+ internal static GUIStyle EventFiltersStyle {
+ get {
+ if (eventFilters == null) {
+ eventFilters = new GUIStyle(EditorStyles.toolbarButton);
+ eventFilters.fontSize = 13;
+ // gets overwritten to content size
+ eventFilters.fixedHeight = 26;
+ eventFilters.fixedWidth = 50;
+ eventFilters.margin = new RectOffset(0, 0, 0, 0);
+ eventFilters.padding = new RectOffset(0, 0, 6, 6);
+ }
+ return eventFilters;
+ }
+ }
+
+ private static Texture2D _clearBackground;
+ private static Texture2D clearBackground {
+ get {
+ if (_clearBackground == null) {
+ _clearBackground = new Texture2D(1, 1);
+ _clearBackground.SetPixel(0, 0, Color.clear);
+ _clearBackground.Apply();
+ }
+ return _clearBackground;
+
+ }
+ }
+
+ public static GUIStyle SectionOuterBoxCompact {
+ get {
+ if (sectionOuterBoxCompactStyle == null) {
+ sectionOuterBoxCompactStyle = new GUIStyle();
+ sectionOuterBoxCompactStyle.padding.top = 10;
+ sectionOuterBoxCompactStyle.padding.bottom = 10;
+ }
+ // Looks better without a background
+ sectionOuterBoxCompactStyle.normal.background = clearBackground;
+ return sectionOuterBoxCompactStyle;
+ }
+ }
+
+ public static GUIStyle SectionInnerBox {
+ get {
+ if (sectionInnerBoxStyle == null) {
+ sectionInnerBoxStyle = new GUIStyle();
+ }
+ sectionInnerBoxStyle.padding = new RectOffset(left: 0, right: 0, top: 15, bottom: 0);
+ return sectionInnerBoxStyle;
+ }
+ }
+
+ public static GUIStyle SectionInnerBoxWide {
+ get {
+ if (sectionInnerBoxWideStyle == null) {
+ sectionInnerBoxWideStyle = new GUIStyle(EditorStyles.helpBox);
+ sectionInnerBoxWideStyle.padding.top = 15;
+ sectionInnerBoxWideStyle.padding.bottom = 15;
+ sectionInnerBoxWideStyle.padding.left = 10;
+ sectionInnerBoxWideStyle.padding.right = 10;
+ }
+ return sectionInnerBoxWideStyle;
+ }
+ }
+
+ public static GUIStyle DynamiSection {
+ get {
+ if (dynamicSection == null) {
+ dynamicSection = new GUIStyle();
+ }
+ var defaultPadding = 13;
+ if (windowScreenWidth > 600) {
+ var dynamicPadding = (windowScreenWidth - 600) / 2;
+ dynamicSection.padding.left = defaultPadding + dynamicPadding;
+ dynamicSection.padding.right = defaultPadding + dynamicPadding;
+ } else if (windowScreenWidth < Constants.IndicationTextHideWidth) {
+ dynamicSection.padding.left = 0;
+ dynamicSection.padding.right = 0;
+ } else {
+ dynamicSection.padding.left = 13;
+ dynamicSection.padding.right = 13;
+ }
+ return dynamicSection;
+ }
+ }
+
+ public static GUIStyle DynamicSectionHelpTab {
+ get {
+ if (dynamicSectionHelpTab == null) {
+ dynamicSectionHelpTab = new GUIStyle(DynamiSection);
+ }
+ dynamicSectionHelpTab.padding.left = DynamiSection.padding.left - 3;
+ dynamicSectionHelpTab.padding.right = DynamiSection.padding.right - 3;
+ return dynamicSectionHelpTab;
+ }
+ }
+
+ public static GUIStyle ChangelogSectionInnerBox {
+ get {
+ if (changelogSectionInnerBoxStyle == null) {
+ changelogSectionInnerBoxStyle = new GUIStyle(EditorStyles.helpBox);
+ changelogSectionInnerBoxStyle.margin.bottom = 10;
+ changelogSectionInnerBoxStyle.margin.top = 10;
+ }
+ return changelogSectionInnerBoxStyle;
+ }
+ }
+
+ public static GUIStyle IndicationBox {
+ get {
+ if (indicationBoxStyle == null) {
+ indicationBoxStyle = new GUIStyle();
+ }
+ indicationBoxStyle.margin.bottom = windowScreenWidth < 141 ? 0 : 10;
+ return indicationBoxStyle;
+ }
+ }
+
+
+ public static GUIStyle LinkStyle {
+ get {
+ if (linkStyle == null) {
+ linkStyle = new GUIStyle(EditorStyles.label);
+ linkStyle.fontStyle = FontStyle.Bold;
+ }
+ var color = IsDarkMode ? DarkModeLinkColor : LightModeModeLinkColor;
+ linkStyle.normal.textColor = color;
+ return linkStyle;
+ }
+ }
+
+ private static Color32 DarkModeLinkColor {
+ get {
+ if (darkModeLinkColor == null) {
+ darkModeLinkColor = new Color32(0x3F, 0x9F, 0xFF, 0xFF);
+ }
+ return darkModeLinkColor.Value;
+ }
+ }
+
+
+ private static Color32 LightModeModeLinkColor {
+ get {
+ if (lightModeModeLinkColor == null) {
+ lightModeModeLinkColor = new Color32(0x0F, 0x52, 0xD7, 0xFF);
+ }
+ return lightModeModeLinkColor.Value;
+ }
+ }
+ public static GUIStyle ProgressBarBarStyle {
+ get {
+ if (progressBarBarStyle != null) {
+ return progressBarBarStyle;
+ }
+ var styles = (EditorStyles)typeof(EditorStyles)
+ .GetField("s_Current", BindingFlags.Static | BindingFlags.NonPublic)
+ ?.GetValue(null);
+ var style = styles?.GetType()
+ .GetField("m_ProgressBarBar", BindingFlags.NonPublic | BindingFlags.Instance)
+ ?.GetValue(styles);
+ progressBarBarStyle = style != null ? (GUIStyle)style : GUIStyle.none;
+ return progressBarBarStyle;
+ }
+ }
+
+ internal static GUIStyle Section {
+ get {
+ if (section == null) {
+ section = new GUIStyle(EditorStyles.helpBox);
+ section.padding = new RectOffset(left: 10, right: 10, top: 10, bottom: 10);
+ section.margin = new RectOffset(left: 0, right: 0, top: 0, bottom: 0);
+ }
+ return section;
+ }
+ }
+ internal static GUIStyle Scroll {
+ get {
+ if (scroll == null) {
+ scroll = new GUIStyle(EditorStyles.helpBox);
+ }
+ if (IsDarkMode) {
+ scroll.normal.background = GUIHelper.ConvertTextureToColor(new Color(0,0,0,0.05f));
+ } else {
+ scroll.normal.background = GUIHelper.ConvertTextureToColor(new Color(0,0,0,0.03f));
+ }
+ return scroll;
+ }
+ }
+
+ internal static GUIStyle BarStyle {
+ get {
+ if (barStyle == null) {
+ barStyle = new GUIStyle(GUI.skin.label);
+ barStyle.fontSize = 12;
+ barStyle.alignment = TextAnchor.MiddleLeft;
+ barStyle.fixedHeight = 20;
+ barStyle.padding = new RectOffset(10, 5, 2, 2);
+ }
+ return barStyle;
+ }
+ }
+
+ internal static GUIStyle BarBackgroundStyle {
+ get {
+ if (barBgStyle == null) {
+ barBgStyle = new GUIStyle();
+ }
+ barBgStyle.normal.background = GUIHelper.ConvertTextureToColor(Color.clear);
+ barBgStyle.hover.background = GUIHelper.ConvertTextureToColor(new Color(0, 0, 0, 0.1f));
+ barBgStyle.focused.background = GUIHelper.ConvertTextureToColor(Color.clear);
+ barBgStyle.active.background = null;
+ return barBgStyle;
+ }
+ }
+
+ internal static GUIStyle ChildBarStyle {
+ get {
+ if (barChildStyle == null) {
+ barChildStyle = new GUIStyle(BarStyle);
+ barChildStyle.padding = new RectOffset(43, barChildStyle.padding.right, barChildStyle.padding.top, barChildStyle.padding.bottom);
+ }
+ return barChildStyle;
+ }
+ }
+
+ internal static GUIStyle FoldoutBarStyle {
+ get {
+ if (barFoldoutStyle == null) {
+ barFoldoutStyle = new GUIStyle(BarStyle);
+ barFoldoutStyle.padding = new RectOffset(23, barFoldoutStyle.padding.right, barFoldoutStyle.padding.top, barFoldoutStyle.padding.bottom);
+ }
+ return barFoldoutStyle;
+ }
+ }
+
+ public static GUIStyle TimestampStyle {
+ get {
+ if (timestampStyle == null) {
+ timestampStyle = new GUIStyle(GUI.skin.label);
+ }
+ if (IsDarkMode) {
+ timestampStyle.normal.textColor = new Color(0.5f, 0.5f, 0.5f);
+ } else {
+ timestampStyle.normal.textColor = new Color(0.5f, 0.5f, 0.5f);
+ }
+ timestampStyle.hover = timestampStyle.normal;
+ return timestampStyle;
+ }
+ }
+
+ internal static GUIStyle ClickableLabelBoldStyle {
+ get {
+ if (clickableLabelBoldStyle == null) {
+ clickableLabelBoldStyle = new GUIStyle(LabelStyle);
+ clickableLabelBoldStyle.fontStyle = FontStyle.Bold;
+ clickableLabelBoldStyle.fontSize = 14;
+ clickableLabelBoldStyle.margin.left = 17;
+ clickableLabelBoldStyle.active.textColor = clickableLabelBoldStyle.normal.textColor;
+ }
+ return clickableLabelBoldStyle;
+ }
+ }
+
+ internal static GUIStyle EmptyListText {
+ get {
+ if (_emptyListText == null) {
+ _emptyListText = new GUIStyle();
+ _emptyListText.fontSize = 11;
+ _emptyListText.padding.left = 15;
+ _emptyListText.padding.top = 10;
+ _emptyListText.alignment = TextAnchor.MiddleCenter;
+ _emptyListText.normal.textColor = Color.gray;
+ }
+
+ return _emptyListText;
+ }
+ }
+
+ internal static GUIStyle StacktraceTextAreaStyle {
+ get {
+ if (_stacktraceTextAreaStyle == null) {
+ _stacktraceTextAreaStyle = new GUIStyle(EditorStyles.textArea);
+ _stacktraceTextAreaStyle.border = new RectOffset(0, 0, 0, 0);
+ }
+ return _stacktraceTextAreaStyle;
+ }
+ }
+
+ internal static GUIStyle EntryBoxStyle {
+ get {
+ if (_entryBoxStyle == null) {
+ _entryBoxStyle = new GUIStyle();
+ _entryBoxStyle.margin.left = 30;
+ }
+ return _entryBoxStyle;
+ }
+ }
+
+ internal static GUIStyle ChildEntryBoxStyle {
+ get {
+ if (_childEntryBoxStyle == null) {
+ _childEntryBoxStyle = new GUIStyle();
+ _childEntryBoxStyle.margin.left = 45;
+ }
+ return _childEntryBoxStyle;
+ }
+ }
+
+ internal static GUIStyle CustomFoldoutStyle {
+ get {
+ if (_customFoldoutStyle == null) {
+ _customFoldoutStyle = new GUIStyle(EditorStyles.foldout);
+ _customFoldoutStyle.margin.top = 4;
+ _customFoldoutStyle.margin.left = 0;
+ _customFoldoutStyle.padding.left = 0;
+ _customFoldoutStyle.fixedWidth = 100;
+ }
+ return _customFoldoutStyle;
+ }
+ }
+
+ internal static GUIStyle RemoveIconStyle {
+ get {
+ if (_removeIconStyle == null) {
+ _removeIconStyle = new GUIStyle();
+ _removeIconStyle.margin.top = 5;
+ _removeIconStyle.fixedWidth = 17;
+ _removeIconStyle.fixedHeight = 17;
+ }
+ return _removeIconStyle;
+ }
+ }
+
+ internal static GUIStyle UpgradeLicenseButtonStyle {
+ get {
+ if (upgradeLicenseButtonStyle == null) {
+ upgradeLicenseButtonStyle = new GUIStyle(GUI.skin.button);
+ upgradeLicenseButtonStyle.padding = new RectOffset(5, 5, 0, 0);
+ }
+ return upgradeLicenseButtonStyle;
+ }
+ }
+
+ internal static GUIStyle UpgradeLicenseButtonOverlayStyle {
+ get {
+ if (upgradeLicenseButtonOverlayStyle == null) {
+ upgradeLicenseButtonOverlayStyle = new GUIStyle(UpgradeLicenseButtonStyle);
+ }
+ return upgradeLicenseButtonOverlayStyle;
+ }
+ }
+
+ internal static GUIStyle UpgradeButtonStyle {
+ get {
+ if (upgradeButtonStyle == null) {
+ upgradeButtonStyle = new GUIStyle(EditorStyles.miniButton);
+ upgradeButtonStyle.fontStyle = FontStyle.Bold;
+ upgradeButtonStyle.fontSize = 14;
+ upgradeButtonStyle.fixedHeight = 24;
+ }
+ return upgradeButtonStyle;
+ }
+ }
+
+ internal static GUIStyle HideButtonStyle {
+ get {
+ if (hideButtonStyle == null) {
+ hideButtonStyle = new GUIStyle(GUI.skin.button);
+ }
+ return hideButtonStyle;
+ }
+ }
+
+ internal static GUIStyle HelpTabButton {
+ get {
+ if (helpTabButton == null) {
+ helpTabButton = new GUIStyle(GUI.skin.button);
+ helpTabButton.alignment = TextAnchor.MiddleLeft;
+ helpTabButton.padding.left = 10;
+ }
+ return helpTabButton;
+ }
+ }
+
+ internal static GUIStyle IndicationHelpBox {
+ get {
+ if (indicationHelpBox == null) {
+ indicationHelpBox = new GUIStyle(EditorStyles.helpBox);
+ indicationHelpBox.margin.right = 0;
+ indicationHelpBox.margin.left = 0;
+ }
+ return indicationHelpBox;
+ }
+ }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Editor/Window/Styles/HotReloadWindowStyles.cs.meta b/Packages/com.singularitygroup.hotreload/Editor/Window/Styles/HotReloadWindowStyles.cs.meta
new file mode 100644
index 0000000..9c7385a
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Editor/Window/Styles/HotReloadWindowStyles.cs.meta
@@ -0,0 +1,18 @@
+fileFormatVersion: 2
+guid: c06a986e9e8c3874f9578f0002ff3a2d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Editor/Window/Styles/HotReloadWindowStyles.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/HotReload.code-workspace b/Packages/com.singularitygroup.hotreload/HotReload.code-workspace
new file mode 100644
index 0000000..c6653fc
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/HotReload.code-workspace
@@ -0,0 +1,11 @@
+{
+ "folders": [
+ {
+ "path": "."
+ },
+ {
+ "path": "../../../script"
+ }
+ ],
+ "settings": {}
+}
\ No newline at end of file
diff --git a/Packages/com.singularitygroup.hotreload/HotReload.code-workspace.meta b/Packages/com.singularitygroup.hotreload/HotReload.code-workspace.meta
new file mode 100644
index 0000000..f1d71a4
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/HotReload.code-workspace.meta
@@ -0,0 +1,14 @@
+fileFormatVersion: 2
+guid: c5fb35f04def51646a584aa23b72d056
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/HotReload.code-workspace
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/LICENSE.md b/Packages/com.singularitygroup.hotreload/LICENSE.md
new file mode 100644
index 0000000..b9eb62e
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/LICENSE.md
@@ -0,0 +1,45 @@
+End User License Agreement (“EULA”) for Hot Reload for Unity (“Software”)
+Please read this End-User License Agreement ("Agreement") carefully before purchasing, downloading, or using Hot Reload for Unity ("Software").
+
+By purchasing, downloading or using the Software, you, the individual or entity (“End-User”), agree to be bound by this EULA as well as by our Terms and Conditions.
+
+If End-User does not agree to the terms of this Agreement, do not purchase, download or use the Software.
+
+The subject matter of this EULA is the licensing of the Software to End-User. The Software is licensed, not sold.
+
+License
+
+The Naughty Cult Ltd. (“Licensor”) grants End-User a revocable, non-exclusive, worldwide, non-transferable, limited license to download, install and use the Software for personal and commercial purposes in accordance with the terms of this Agreement and the terms set out in the Terms and Conditions.
+
+The Software is owned and copyrighted by The Naughty Cult Ltd.. Your license confers no title or ownership in the Software and should not be construed as a sale of any right in the Software.
+
+The Software is protected by copyright law and international treaty provisions. You acknowledge that no ownership of the intellectual property in the Software is transferred to you. You further acknowledge that The Naughty Cult Ltd. retains full ownership rights to the Software, and you will not acquire any rights to the Software except as outlined in this license. You agree that any copies of the Software will include the same proprietary notices found on and within the original Software.
+
+End-User's Rights and Obligations
+
+End-User may use the licensed Software only for its intended purpose. End-User may not modify, reproduce, distribute, sublicense, rent, lease or lend the Software.
+Each active license allows End-User to install and use the Software on a maximum of two devices associated with one specific Unity seat. End-User may not share the Software or the license key with any third party.
+
+You may not modify the Software or disable any licensing or control features of the Software without express permission from the Licensor. You agree that you will not attempt to reverse compile, modify, translate, or disassemble the Software in whole or in part.
+
+Once End-User's active license is terminated, End-User will not receive any new updates to the Software, and may not download, install, integrate or otherwise use versions of the Software released at any time hereafter, unless a license is activated.
+
+Termination
+This EULA will terminate automatically if End-User fails to comply with any of the terms and conditions of this Agreement. In such event, End-User must immediately stop using the Software and destroy all copies of the Software in End-User's possession.
+
+Governing Law
+This EULA shall be governed by the laws of the country in which the Licensor is headquartered without regard to its conflict of law provisions. We reserve the right to terminate or suspend your account, without notice or liability, for any reason, including breach of the Terms and Conditions and/or EULA.
+
+Limitation of Liability
+The Naughty Cult Ltd. and its affiliates shall not be held liable for any indirect, incidental, special, consequential or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from your use of or inability to use the Service, any conduct or content of any third party on the Service, any content obtained from the Service, or unauthorized access or alteration of your transmissions or content. This limitation applies regardless of whether the damages are based on warranty, contract, tort (including negligence), or any other legal theory, and even if we have been advised of the possibility of such damages.
+
+
+Disclaimer of Warranties
+
+The Service is provided on an "as is" and "as available" basis without any warranties of any kind, either express or implied. We do not warrant that the Service will be uninterrupted or error-free, or that any defects will be corrected. We also do not guarantee that the Service will meet your requirements.
+
+Waiver and Severability
+Our failure to enforce any right or provision of this EULA will not be deemed a waiver of such right or provision. In the event that any provision of these EULA is held to be invalid or unenforceable, the remaining provisions will remain in full force and effect.
+
+Entire Agreement
+This EULA constitutes the entire agreement between End-User and Licensor regarding the use of the Software and supersedes all prior agreements and understandings, whether written or oral.
diff --git a/Packages/com.singularitygroup.hotreload/LICENSE.md.meta b/Packages/com.singularitygroup.hotreload/LICENSE.md.meta
new file mode 100644
index 0000000..e8d5657
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/LICENSE.md.meta
@@ -0,0 +1,14 @@
+fileFormatVersion: 2
+guid: 0f0ed454ae8a66041bea966cdcee0f2e
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/LICENSE.md
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/README.md b/Packages/com.singularitygroup.hotreload/README.md
new file mode 100644
index 0000000..1c30967
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/README.md
@@ -0,0 +1,9 @@
+
+
+# Hot Reload for Unity
+
+Edit **any C# function** and get immediate updates in your game. Hot Reload works with your existing project, no code changes required.
+
+Install instructions on https://hotreload.net/
+
+
diff --git a/Packages/com.singularitygroup.hotreload/README.md.meta b/Packages/com.singularitygroup.hotreload/README.md.meta
new file mode 100644
index 0000000..4a80df0
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/README.md.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: bdb53603710c4ae3b491b7885e5ff702
+timeCreated: 1674514875
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/README.md
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Runtime.meta b/Packages/com.singularitygroup.hotreload/Runtime.meta
new file mode 100644
index 0000000..2b0c282
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Runtime.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 8026562867072c3409c904654ec3c17f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/com.singularitygroup.hotreload/Runtime/AppCallbackListener.cs b/Packages/com.singularitygroup.hotreload/Runtime/AppCallbackListener.cs
new file mode 100644
index 0000000..77618a6
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Runtime/AppCallbackListener.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections;
+using UnityEngine;
+
+namespace SingularityGroup.HotReload {
+ class AppCallbackListener : MonoBehaviour {
+ ///
+ /// Reliable on Android and in the editor.
+ ///
+ ///
+ /// On iOS, OnApplicationPause is not called at expected moments
+ /// if the app has some background modes enabled in PlayerSettings -Troy.
+ ///
+ public static event Action onApplicationPause;
+
+ ///
+ /// Reliable on Android, iOS and in the editor.
+ ///
+ public static event Action onApplicationFocus;
+
+ static AppCallbackListener instance;
+ public static AppCallbackListener I => instance;
+
+ // Must be called early from Unity main thread (before any usages of the singleton I).
+ public static AppCallbackListener Init() {
+ if(instance) return instance;
+ var go = new GameObject("AppCallbackListener");
+ go.hideFlags |= HideFlags.HideInHierarchy;
+ DontDestroyOnLoad(go);
+ return instance = go.AddComponent();
+ }
+
+ public bool Paused { get; private set; } = false;
+
+ public void DelayedQuit(float seconds) {
+ StartCoroutine(delayedQuitRoutine(seconds));
+ }
+
+ IEnumerator delayedQuitRoutine(float seconds) {
+ yield return new WaitForSeconds(seconds);
+ Application.Quit();
+ }
+
+ void OnApplicationPause(bool paused) {
+ Paused = paused;
+ onApplicationPause?.Invoke(paused);
+ }
+
+ void OnApplicationFocus(bool playing) {
+ onApplicationFocus?.Invoke(playing);
+ }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Runtime/AppCallbackListener.cs.meta b/Packages/com.singularitygroup.hotreload/Runtime/AppCallbackListener.cs.meta
new file mode 100644
index 0000000..0d7b6ec
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Runtime/AppCallbackListener.cs.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: a989a17330b04c6fb8f91aa41ac14471
+timeCreated: 1674216227
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Runtime/AppCallbackListener.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Runtime/BuildInfo.cs b/Packages/com.singularitygroup.hotreload/Runtime/BuildInfo.cs
new file mode 100644
index 0000000..392ad0f
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Runtime/BuildInfo.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using JetBrains.Annotations;
+using SingularityGroup.HotReload.Newtonsoft.Json;
+using UnityEngine;
+using UnityEngine.Serialization;
+
+namespace SingularityGroup.HotReload {
+ ///
+ /// Information about the Unity Player build.
+ ///
+ ///
+ ///
+ /// This info is used by the HotReload Server to compile your project in the same way that the Unity Player build was compiled.
+ /// For example, when building for Android, Unity sets a bunch of define symbols like UNITY_ANDROID.
+ ///
+ ///
+ /// Information that changes between builds is generated at build-time and put in StreamingAssets/.
+ /// This approach means that builds do not need to modify a project file (making file dirty in git). For example,
+ /// whenever user makes a mono build, the CommitHash changes and we need to regenerate the BuildInfo.
+ ///
+ ///
+ [Serializable]
+ class BuildInfo {
+ ///
+ /// Uniquely identifies the Unity project.
+ ///
+ ///
+ /// Used on-device to check if Hot Reload server is compatible with the Unity project (same project).
+ /// When your computer has multiple Unity projects open, each project should provide a different value.
+ /// This identifier must also be the same between two different computers that are collaborating on the same project.
+ ///
+ ///
+ /// Edge-case: when a user copy pastes an entire Unity project and has both open at once,
+ /// then it's fine for this identifier to be the same.
+ ///
+ ///
+ public string projectIdentifier;
+
+ ///
+ /// Git commit hash
+ ///
+ ///
+ /// Used to detect that your code is different to when the build was made.
+ ///
+ public string commitHash;
+
+ ///
+ /// List of define symbols that were active when this build was made.
+ ///
+ ///
+ /// Separate the symbols with a semi-colon character ';'
+ ///
+ public string defineSymbols;
+
+ ///
+ /// A regex of C# project names (*.csproj) to be omitted from compilation.
+ ///
+ ///
+ /// "MyTests|MyEditorAssembly"
+ ///
+ [FormerlySerializedAs("projectExclusionRegex")]
+ public string projectOmissionRegex;
+
+ ///
+ /// The computer that made the Android (or Standalone etc) build.
+ /// The hostname (ip address) where Hot Reload server would be listening.
+ ///
+ public string buildMachineHostName;
+
+ ///
+ /// The computer that made the Android (or Standalone etc) build.
+ /// The port where Hot Reload server would be listening.
+ ///
+ public int buildMachinePort;
+
+ ///
+ /// Selected build target in Unity Editor.
+ ///
+ public string activeBuildTarget;
+
+ ///
+ /// Used to pass in the origin onto the phone which is used to identify the correct server.
+ ///
+ public string buildMachineRequestOrigin;
+
+ ///
+ /// Used to define which language the package is translated to
+ ///
+ public string locale;
+
+ [JsonIgnore]
+ public HashSet DefineSymbolsAsHashSet {
+ get {
+ var symbols = defineSymbols.Trim().Split(';');
+ // split on an empty string produces 1 empty string
+ if (symbols.Length == 1 && symbols[0] == string.Empty) {
+ return new HashSet();
+ }
+ return new HashSet(symbols);
+ }
+ }
+
+ [JsonIgnore]
+ public PatchServerInfo BuildMachineServer {
+ get {
+ if (buildMachineHostName == null || buildMachinePort == 0) {
+ return null;
+ }
+ return new PatchServerInfo(buildMachineHostName, buildMachinePort, commitHash, null, customRequestOrigin: buildMachineRequestOrigin);
+ }
+ }
+
+ public string ToJson() {
+ return JsonConvert.SerializeObject(this);
+ }
+
+ [CanBeNull]
+ public static BuildInfo FromJson(string json) {
+ if (string.IsNullOrEmpty(json)) {
+ return null;
+ }
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ ///
+ /// Path to read/write the json file to.
+ ///
+ /// A filepath that is inside the player build
+ public static string GetStoredPath() {
+ return Path.Combine(Application.streamingAssetsPath, GetStoredName());
+ }
+
+ public static string GetStoredName() {
+ return "HotReload_BuildInfo.json";
+ }
+
+ /// True if the commit hashes are definately different, otherwise False
+ public bool IsDifferentCommit(string remoteCommit) {
+ if (commitHash == PatchServerInfo.UnknownCommitHash) {
+ return false;
+ }
+
+ return !SameCommit(commitHash, remoteCommit);
+ }
+
+ ///
+ /// Checks whether the commits are equivalent.
+ ///
+ ///
+ ///
+ /// False if the commit hashes are definately different, otherwise True
+ public static bool SameCommit(string commitA, string commitB) {
+ if (commitA == null) {
+ // unknown commit hash, so approve anything
+ return true;
+ }
+
+ if (commitA.Length == commitB.Length) {
+ return commitA == commitB;
+ } else if (commitA.Length >= 6 && commitB.Length >= 6) {
+ // depending on OS, the git log pretty output has different length (7 or 8 chars)
+ // if the longer hash starts with the shorter hash, return true
+ // Assumption: commits have different length.
+ var longer = commitA.Length > commitB.Length ? commitA : commitB;
+ var shorter = commitA.Length > commitB.Length ? commitB : commitA;
+
+ return longer.StartsWith(shorter);
+ }
+ return false;
+ }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Runtime/BuildInfo.cs.meta b/Packages/com.singularitygroup.hotreload/Runtime/BuildInfo.cs.meta
new file mode 100644
index 0000000..64478fd
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Runtime/BuildInfo.cs.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 39bb7d4cd9324f31b1882354b1cde762
+timeCreated: 1673776105
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Runtime/BuildInfo.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Runtime/Burst.meta b/Packages/com.singularitygroup.hotreload/Runtime/Burst.meta
new file mode 100644
index 0000000..f58389b
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Runtime/Burst.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d10d24dc13744197a80f50ac50f5d1a1
+timeCreated: 1675449699
\ No newline at end of file
diff --git a/Packages/com.singularitygroup.hotreload/Runtime/Burst/JobHotReloadUtility.cs b/Packages/com.singularitygroup.hotreload/Runtime/Burst/JobHotReloadUtility.cs
new file mode 100644
index 0000000..a0debd1
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Runtime/Burst/JobHotReloadUtility.cs
@@ -0,0 +1,25 @@
+#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
+using System;
+using System.Reflection;
+using SingularityGroup.HotReload.DTO;
+
+namespace SingularityGroup.HotReload.Burst {
+ public static class JobHotReloadUtility {
+ public static void HotReloadBurstCompiledJobs(SUnityJob jobData, Type proxyJobType) {
+ JobPatchUtility.PatchBurstCompiledJobs(jobData, proxyJobType, unityMajorVersion:
+ #if UNITY_2022_2_OR_NEWER
+ 2022
+ #elif UNITY_2021_3_OR_NEWER
+ 2021
+ #elif UNITY_2020_3_OR_NEWER
+ 2020
+ #elif UNITY_2019_4_OR_NEWER
+ 2019
+ #else
+ 2018
+ #endif
+ );
+ }
+ }
+}
+#endif
diff --git a/Packages/com.singularitygroup.hotreload/Runtime/Burst/JobHotReloadUtility.cs.meta b/Packages/com.singularitygroup.hotreload/Runtime/Burst/JobHotReloadUtility.cs.meta
new file mode 100644
index 0000000..887d007
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Runtime/Burst/JobHotReloadUtility.cs.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: b9980b40e3ff447b94e71de238a37fb7
+timeCreated: 1676825622
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Runtime/Burst/JobHotReloadUtility.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Runtime/BurstChecker.cs b/Packages/com.singularitygroup.hotreload/Runtime/BurstChecker.cs
new file mode 100644
index 0000000..995d1fc
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Runtime/BurstChecker.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace SingularityGroup.HotReload {
+ static class BurstChecker {
+ //Use names instead of the types directly for compat with older unity versions
+ const string whitelistAttrName = "BurstCompileAttribute";
+ const string blacklistAttrName = "BurstDiscardAttribute";
+
+ public static bool IsBurstCompiled(MethodBase method) {
+ //blacklist has precedence over whitelist
+ if(HasAttr(method.GetCustomAttributes(), blacklistAttrName)) {
+ return false;
+ }
+ if(HasAttr(method.GetCustomAttributes(), whitelistAttrName)) {
+ return true;
+ }
+ //Static methods inside a [BurstCompile] type are not burst compiled by default
+ if(method.DeclaringType == null || method.IsStatic) {
+ return false;
+ }
+ if(HasAttr(method.DeclaringType.GetCustomAttributes(), whitelistAttrName)) {
+ return true;
+ }
+ //No matching attributes
+ return false;
+ }
+
+ static bool HasAttr(IEnumerable attributes, string name) {
+ foreach (var attr in attributes) {
+ if(attr.GetType().Name == name) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/Packages/com.singularitygroup.hotreload/Runtime/BurstChecker.cs.meta b/Packages/com.singularitygroup.hotreload/Runtime/BurstChecker.cs.meta
new file mode 100644
index 0000000..075f40c
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Runtime/BurstChecker.cs.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 20dfd902e9fc4485aeef90b9add39c0a
+timeCreated: 1675404225
+AssetOrigin:
+ serializedVersion: 1
+ productId: 254358
+ packageName: Hot Reload | Edit Code Without Compiling
+ packageVersion: 1.13.17
+ assetPath: Packages/com.singularitygroup.hotreload/Runtime/BurstChecker.cs
+ uploadId: 870414
diff --git a/Packages/com.singularitygroup.hotreload/Runtime/CodePatcher.cs b/Packages/com.singularitygroup.hotreload/Runtime/CodePatcher.cs
new file mode 100644
index 0000000..7f7999d
--- /dev/null
+++ b/Packages/com.singularitygroup.hotreload/Runtime/CodePatcher.cs
@@ -0,0 +1,770 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using SingularityGroup.HotReload.DTO;
+using SingularityGroup.HotReload.Localization;
+using JetBrains.Annotations;
+using SingularityGroup.HotReload.Burst;
+using SingularityGroup.HotReload.HarmonyLib;
+using SingularityGroup.HotReload.JsonConverters;
+using SingularityGroup.HotReload.MonoMod.Utils;
+using SingularityGroup.HotReload.Newtonsoft.Json;
+using SingularityGroup.HotReload.RuntimeDependencies;
+#if UNITY_EDITOR
+using UnityEditor;
+using UnityEditorInternal;
+#endif
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+[assembly: InternalsVisibleTo("SingularityGroup.HotReload.Editor")]
+
+namespace SingularityGroup.HotReload {
+ class RegisterPatchesResult {
+ // note: doesn't include removals and method definition changes (e.g. renames)
+ public readonly List patchedMethods = new List();
+ public List addedFields = new List();
+ public readonly List patchedSMethods = new List();
+ public bool inspectorModified;
+ public bool inspectorFieldAdded;
+ public readonly List> patchFailures = new List>();
+ public readonly List patchExceptions = new List();
+ }
+
+ class FieldHandler {
+ public readonly Func storeField;
+ public readonly Action registerInspectorFieldAttributes;
+ public readonly Func hideField;
+
+ public FieldHandler(Func storeField, Func hideField, Action registerInspectorFieldAttributes) {
+ this.storeField = storeField;
+ this.hideField = hideField;
+ this.registerInspectorFieldAttributes = registerInspectorFieldAttributes;
+ }
+ }
+
+ class CodePatcher {
+ public static readonly CodePatcher I = new CodePatcher();
+ /// Tag for use in Debug.Log.
+ public const string TAG = "HotReload";
+
+ internal int PatchesApplied { get; private set; }
+ string PersistencePath {get;}
+
+ List pendingPatches;
+ readonly List patchHistory;
+ readonly HashSet seenResponses = new HashSet();
+ string[] assemblySearchPaths;
+ SymbolResolver symbolResolver;
+ readonly string tmpDir;
+ public FieldHandler fieldHandler;
+ public bool debuggerCompatibilityEnabled;
+ public bool anyFailures;
+
+ public IReadOnlyList PatchHistory => patchHistory;
+
+ CodePatcher() {
+ pendingPatches = new List();
+ patchHistory = new List();
+ if(UnityHelper.IsEditor) {
+ tmpDir = PackageConst.LibraryCachePath;
+ } else {
+ tmpDir = UnityHelper.TemporaryCachePath;
+ }
+ if(!UnityHelper.IsEditor) {
+ PersistencePath = Path.Combine(UnityHelper.PersistentDataPath, "HotReload", "patches.json");
+ try {
+ LoadPatches(PersistencePath);
+ } catch(Exception ex) {
+ Log.Error($"{Localization.Translations.Logging.LoadingPatchesFromDiskError}\n{ex}");
+ }
+ } else {
+ PersistencePath = Path.Combine(PackageConst.LibraryCachePath, "patches.json");
+ }
+#if UNITY_EDITOR
+ // Unity event methods are not assigned outside the scene.
+ // So we need to ensure they are added when entering play mode from edit mode
+ EditorApplication.playModeStateChanged += state => {
+ if (state != PlayModeStateChange.EnteredPlayMode) {
+ return;
+ }
+ foreach (var unityEventMethod in unityEventMethods) {
+ EnsureUnityEventMethod(unityEventMethod);
+ }
+ };
+#endif
+ }
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
+ static void InitializeUnityEvents() {
+ UnityEventHelper.Initialize();
+ }
+
+
+ void LoadPatches(string filePath) {
+ PlayerLog(Localization.Translations.Logging.LoadingPatchesFromFile, filePath);
+ var file = new FileInfo(filePath);
+ if(file.Exists) {
+ var bytes = File.ReadAllText(filePath);
+ var patches = JsonConvert.DeserializeObject>(bytes);
+ PlayerLog(Localization.Translations.Logging.LoadedPatchesFromDisk, patches.Count.ToString());
+ foreach (var patch in patches) {
+ RegisterPatches(patch, persist: false);
+ }
+ }
+ }
+
+
+ internal IReadOnlyList PendingPatches => pendingPatches;
+ internal SymbolResolver SymbolResolver => symbolResolver;
+
+
+ internal string[] GetAssemblySearchPaths() {
+ EnsureSymbolResolver();
+ return assemblySearchPaths;
+ }
+
+ internal void RegisterFailures(MethodPatchResponse patch, RegisterPatchesResult result) {
+ anyFailures |= patch.failures?.Length > 0 || result?.patchFailures.Count > 0 || result?.patchExceptions.Count > 0;
+ }
+
+ internal RegisterPatchesResult RegisterPatches(MethodPatchResponse patches, bool persist) {
+ PlayerLog(Localization.Translations.Logging.RegisterPatches, string.Join("\n", patches.failures), string.Join("\n", patches.patches.SelectMany(p => p.modifiedMethods).Select(m => m.displayName)));
+ pendingPatches.Add(patches);
+ return ApplyPatches(persist);
+ }
+
+ RegisterPatchesResult ApplyPatches(bool persist) {
+ PlayerLog(Localization.Translations.Logging.ApplyPatchesPending, pendingPatches.Count);
+ EnsureSymbolResolver();
+
+ var result = new RegisterPatchesResult();
+
+ try {
+ int count = 0;
+ foreach(var response in pendingPatches) {
+ if (seenResponses.Contains(response.id)) {
+ continue;
+ }
+ foreach (var patch in response.patches) {
+ var asm = Assembly.Load(patch.patchAssembly, patch.patchPdb);
+ SymbolResolver.AddAssembly(asm);
+ }
+ HandleRemovedUnityMethods(response.removedMethod);
+#if UNITY_EDITOR
+ HandleAlteredFields(response.id, result, response.alteredFields);
+#endif
+ // needs to come before RegisterNewFieldInitializers
+ RegisterNewFieldDefinitions(response);
+ // Note: order is important here. Reshaped fields require new field initializers to be added
+ // because the old initializers must override new initilaizers for existing holders.
+ // so that the initializer is not invoked twice
+ RegisterNewFieldInitializers(response);
+ HandleReshapedFields(response);
+ RemoveOldFieldInitializers(response);
+#if UNITY_EDITOR
+ RegisterInspectorFieldAttributes(result, response);
+#endif
+
+ HandleMethodPatchResponse(response, result);
+ patchHistory.Add(response);
+
+ seenResponses.Add(response.id);
+ count += response.patches.Length;
+ }
+ if (count > 0) {
+ Dispatch.OnHotReload(result.patchedMethods).Forget();
+ }
+ } catch(Exception ex) {
+ Log.Warning($"{Localization.Translations.Logging.ExceptionHandlingMethodPatch}\n{ex}");
+ } finally {
+ pendingPatches.Clear();
+ }
+
+ if(PersistencePath != null && persist) {
+ SaveAppliedPatches(PersistencePath).Forget();
+ }
+
+ PatchesApplied++;
+ return result;
+ }
+
+ internal void ClearPatchedMethods() {
+ PatchesApplied = 0;
+ }
+
+ static bool didLog;
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
+ static void WarnOnSceneLoad() {
+ SceneManager.sceneLoaded += (_, __) => {
+ if (didLog || !UnityEventHelper.UnityMethodsAdded()) {
+ return;
+ }
+ Log.Warning(Localization.Translations.Logging.SceneLoadedWithNewUnityEventMethods);
+ didLog = true;
+ };
+ }
+
+ static HashSet unityEventMethods = new HashSet();
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
+ static void OnSceneLoad() {
+ SceneManager.sceneLoaded += (_, __) => {
+ foreach (var unityEventMethod in unityEventMethods) {
+ EnsureUnityEventMethod(unityEventMethod);
+ }
+ };
+ }
+
+ static bool EnsureUnityEventMethod(MethodBase newMethod) {
+ try {
+ return UnityEventHelper.EnsureUnityEventMethod(newMethod);
+ } catch(Exception ex) {
+ Log.Warning(Localization.Translations.Logging.ExceptionEnsureUnityEventMethod, ex.GetType().Name, ex.Message);
+ return false;
+ }
+ }
+
+ void HandleMethodPatchResponse(MethodPatchResponse response, RegisterPatchesResult result) {
+ EnsureSymbolResolver();
+
+ foreach(var patch in response.patches) {
+ try {
+ foreach(var sMethod in patch.newMethods) {
+ var newMethod = SymbolResolver.Resolve(sMethod);
+
+ var isUnityEvent = EnsureUnityEventMethod(newMethod);
+ if (isUnityEvent) {
+ unityEventMethods.Add(newMethod);
+ }
+
+ MethodUtils.DisableVisibilityChecks(newMethod);
+ if (!patch.patchMethods.Any(m => m.metadataToken == sMethod.metadataToken)) {
+ result.patchedMethods.Add(new MethodPatch(null, null, newMethod));
+ result.patchedSMethods.Add(sMethod);
+ previousPatchMethods[newMethod] = newMethod;
+ newMethods.Add(newMethod);
+ }
+ }
+
+ for (int i = 0; i < patch.modifiedMethods.Length; i++) {
+ var sOriginalMethod = patch.modifiedMethods[i];
+ var sPatchMethod = patch.patchMethods[i];
+ var err = PatchMethod(response.id, sOriginalMethod: sOriginalMethod, sPatchMethod: sPatchMethod, containsBurstJobs: patch.unityJobs.Length > 0, patchesResult: result);
+ if (!string.IsNullOrEmpty(err)) {
+ result.patchFailures.Add(Tuple.Create(sOriginalMethod, err));
+ }
+ }
+ foreach (var job in patch.unityJobs) {
+ var type = SymbolResolver.Resolve(new SType(patch.assemblyName, job.jobKind.ToString(), job.metadataToken));
+ JobHotReloadUtility.HotReloadBurstCompiledJobs(job, type);
+ }
+#if UNITY_EDITOR
+ HandleNewFields(patch.patchId, result, patch.newFields);
+#endif
+ } catch (Exception ex) {
+ RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.Exception), new EditorExtraData {
+ { StatKey.PatchId, patch.patchId },
+ { StatKey.Detailed_Exception, ex.ToString() },
+ }).Forget();
+ result.patchExceptions.Add($"{Localization.Translations.Logging.ExceptionApplyingPatch}\nException: {ex}");
+ }
+ }
+ }
+
+ void HandleRemovedUnityMethods(SMethod[] removedMethods) {
+ if (removedMethods == null) {
+ return;
+ }
+ foreach(var sMethod in removedMethods) {
+ try {
+ var oldMethod = SymbolResolver.Resolve(sMethod);
+ UnityEventHelper.RemoveUnityEventMethod(oldMethod);
+ unityEventMethods.Remove(oldMethod);
+ } catch (SymbolResolvingFailedException) {
+ // ignore, not a unity event method if can't resolve
+ } catch(Exception ex) {
+ Log.Warning(Localization.Translations.Logging.ExceptionRemoveUnityEventMethod, ex.GetType().Name, ex.Message);
+ }
+ }
+ }
+
+ // Important: must come before applying any patches
+ void RegisterNewFieldInitializers(MethodPatchResponse resp) {
+ for (var i = 0; i < resp.addedFieldInitializerFields.Length; i++) {
+ var sField = resp.addedFieldInitializerFields[i];
+ var sMethod = resp.addedFieldInitializerInitializers[i];
+ try {
+ var declaringType = SymbolResolver.Resolve(sField.declaringType);
+ var method = SymbolResolver.Resolve(sMethod);
+ if (!(method is MethodInfo initializer)) {
+ Log.Warning(string.Format(Localization.Translations.Logging.FailedRegisteringInitializerInvalidMethod, sField.fieldName, sField.declaringType.typeName));
+ continue;
+ }
+ // We infer if the field is static by the number of parameters the method has
+ // because sField is old field
+ var isStatic = initializer.GetParameters().Length == 0;
+ MethodUtils.DisableVisibilityChecks(initializer);
+ // Initializer return type is used in place of fieldType because latter might be point to old field if the type changed
+ FieldInitializerRegister.RegisterInitializer(declaringType, sField.fieldName, initializer.ReturnType, initializer, isStatic);
+
+ } catch (Exception e) {
+ RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.RegisterFieldInitializer), new EditorExtraData {
+ { StatKey.PatchId, resp.id },
+ { StatKey.Detailed_Exception, e.ToString() },
+ }).Forget();
+ Log.Warning(string.Format(Localization.Translations.Logging.FailedRegisteringInitializerException, sField.fieldName, sField.declaringType.typeName, e.Message));
+ }
+ }
+ }
+
+ void RegisterNewFieldDefinitions(MethodPatchResponse resp) {
+ foreach (var sField in resp.newFieldDefinitions) {
+ try {
+ var declaringType = SymbolResolver.Resolve(sField.declaringType);
+ var fieldType = SymbolResolver.Resolve(sField).FieldType;
+ FieldResolver.RegisterFieldType(declaringType, sField.fieldName, fieldType);
+ } catch (Exception e) {
+ RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.RegisterFieldDefinition), new EditorExtraData {
+ { StatKey.PatchId, resp.id },
+ { StatKey.Detailed_Exception, e.ToString() },
+ }).Forget();
+ Log.Warning(string.Format(Localization.Translations.Logging.FailedRegisteringNewFieldDefinitions, sField.fieldName, sField.declaringType.typeName, e.Message));
+ }
+ }
+ }
+
+ // Important: must come before applying any patches
+ // Note: server might decide not to report removed field initializer at all if it can handle it
+ void RemoveOldFieldInitializers(MethodPatchResponse resp) {
+ foreach (var sField in resp.removedFieldInitializers) {
+ try {
+ var declaringType = SymbolResolver.Resolve(sField.declaringType);
+ var fieldType = SymbolResolver.Resolve(sField.declaringType);
+ FieldInitializerRegister.UnregisterInitializer(declaringType, sField.fieldName, fieldType, sField.isStatic);
+ } catch (Exception e) {
+ RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.UnregisterFieldInitializer), new EditorExtraData {
+ { StatKey.PatchId, resp.id },
+ { StatKey.Detailed_Exception, e.ToString() },
+ }).Forget();
+ Log.Warning(string.Format(Localization.Translations.Logging.FailedRemovingInitializer, sField.fieldName, sField.declaringType.typeName, e.Message));
+ }
+ }
+ }
+
+ // Important: must come before applying any patches
+ // Should also come after RegisterNewFieldInitializers so that new initializers are not invoked for existing objects
+ internal void HandleReshapedFields(MethodPatchResponse resp) {
+ foreach(var patch in resp.patches) {
+ var removedReshapedFields = patch.deletedFields;
+ var renamedReshapedFieldsFrom = patch.renamedFieldsFrom;
+ var renamedReshapedFieldsTo = patch.renamedFieldsTo;
+
+ foreach (var f in removedReshapedFields) {
+ try {
+ var declaringType = SymbolResolver.Resolve(f.declaringType);
+ var fieldType = SymbolResolver.Resolve(f).FieldType;
+ FieldResolver.ClearHolders(declaringType, f.isStatic, f.fieldName, fieldType);
+ } catch (Exception e) {
+ RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.ClearHolders), new EditorExtraData {
+ { StatKey.PatchId, resp.id },
+ { StatKey.Detailed_Exception, e.ToString() },
+ }).Forget();
+ Log.Warning(string.Format(Localization.Translations.Logging.FailedRemovingFieldValue, f.fieldName, f.declaringType.typeName, e.Message));
+ }
+ }
+ for (var i = 0; i < renamedReshapedFieldsFrom.Length; i++) {
+ var fromField = renamedReshapedFieldsFrom[i];
+ var toField = renamedReshapedFieldsTo[i];
+ try {
+ var declaringType = SymbolResolver.Resolve(fromField.declaringType);
+ var fieldType = SymbolResolver.Resolve(fromField).FieldType;
+ var toFieldType = SymbolResolver.Resolve(toField).FieldType;
+ if (!AreSTypesCompatible(fromField.declaringType, toField.declaringType)
+ || fieldType != toFieldType
+ || fromField.isStatic != toField.isStatic
+ ) {
+ FieldResolver.ClearHolders(declaringType, fromField.isStatic, fromField.fieldName, fieldType);
+ continue;
+ }
+ FieldResolver.MoveHolders(declaringType, fromField.fieldName, toField.fieldName, fieldType, fromField.isStatic);
+ } catch (Exception e) {
+ RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.MoveHolders), new EditorExtraData {
+ { StatKey.PatchId, resp.id },
+ { StatKey.Detailed_Exception, e.ToString() },
+ }).Forget();
+ Log.Warning(Localization.Translations.Logging.FailedMovingFieldValue, fromField, toField, toField.declaringType.typeName, e.Message);
+ }
+ }
+ }
+ }
+
+ internal bool AreSTypesCompatible(SType one, SType two) {
+ if (one.isGenericParameter != two.isGenericParameter) {
+ return false;
+ }
+ if (one.metadataToken != two.metadataToken) {
+ return false;
+ }
+ if (one.assemblyName != two.assemblyName) {
+ return false;
+ }
+ if (one.genericParameterPosition != two.genericParameterPosition) {
+ return false;
+ }
+ if (one.typeName != two.typeName) {
+ return false;
+ }
+ return true;
+ }
+
+#if UNITY_EDITOR
+ internal void RegisterInspectorFieldAttributes(RegisterPatchesResult result, MethodPatchResponse resp) {
+ foreach (var patch in resp.patches) {
+ var propertyAttributesFieldOriginal = patch.propertyAttributesFieldOriginal ?? Array.Empty