[Add] Hot Reload

This commit is contained in:
2026-02-27 03:16:18 +07:00
parent 5067cb51a1
commit b37579153b
431 changed files with 43054 additions and 1 deletions
@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using UnityEditor;
using System.Linq;
using System.Runtime.CompilerServices;
using SingularityGroup.HotReload.Editor.Localization;
using SingularityGroup.HotReload.Newtonsoft.Json;
using UnityEditor.Compilation;
[assembly: InternalsVisibleTo("SingularityGroup.HotReload.EditorTests")]
namespace SingularityGroup.HotReload.Editor {
internal static class AssemblyOmission {
// [MenuItem("Window/Hot Reload Dev/List omitted projects")]
private static void Check() {
Log.Info(Translations.Errors.InfoOmitProjectsForPlayerBuild);
var omitted = GetOmittedProjects(EditorUserBuildSettings.activeScriptCompilationDefines);
Log.Info(Translations.Errors.InfoSeparator);
foreach (var name in omitted) {
Log.Info(Translations.Errors.InfoOmittedEditorProject, name);
}
}
[JsonObject(MemberSerialization.Fields)]
private class AssemblyDefinitionJson {
public string name;
public string[] defineConstraints;
}
// scripts in Assets/ (with no asmdef) are always compiled into Assembly-CSharp
private static readonly string alwaysIncluded = "Assembly-CSharp";
private class Cache : AssetPostprocessor {
public static string[] ommitedProjects;
private static void OnPostprocessAllAssets(string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths) {
ommitedProjects = null;
}
}
// main thread only
public static string[] GetOmittedProjects(string allDefineSymbols, bool verboseLogs = false) {
if (Cache.ommitedProjects != null) {
return Cache.ommitedProjects;
}
var arr = allDefineSymbols.Split(';');
var omitted = GetOmittedProjects(arr, verboseLogs);
Cache.ommitedProjects = omitted;
return omitted;
}
// must be deterministic (return projects in same order each time)
private static string[] GetOmittedProjects(string[] allDefineSymbols, bool verboseLogs = false) {
// HotReload uses names of assemblies.
var editorAssemblies = GetEditorAssemblies();
editorAssemblies.Remove(alwaysIncluded);
var omittedByConstraint = DefineConstraints.GetOmittedAssemblies(allDefineSymbols);
editorAssemblies.AddRange(omittedByConstraint);
// Note: other platform player assemblies are also returned here, but I haven't seen it cause issues
// when using Hot Reload with IdleGame Android build.
var playerAssemblies = GetPlayerAssemblies().ToArray();
if (verboseLogs) {
foreach (var name in editorAssemblies) {
Log.Info(Translations.Errors.InfoFoundProjectNamed, name);
}
foreach (var playerAssemblyName in playerAssemblies) {
Log.Debug(string.Format(Translations.Utility.PlayerAssemblyDebug, playerAssemblyName));
}
}
// leaves the editor assemblies that are not built into player assemblies (e.g. editor and test assemblies)
var toOmit = editorAssemblies.Except(playerAssemblies.Select(asm => asm.name));
var unique = new HashSet<string>(toOmit);
return unique.OrderBy(s => s).ToArray();
}
// main thread only
public static List<string> GetEditorAssemblies() {
return CompilationPipeline
.GetAssemblies(AssembliesType.Editor)
.Select(asm => asm.name)
.ToList();
}
public static Assembly[] GetPlayerAssemblies() {
var playerAssemblyNames = CompilationPipeline
#if UNITY_2019_3_OR_NEWER
.GetAssemblies(AssembliesType.PlayerWithoutTestAssemblies) // since Unity 2019.3
#else
.GetAssemblies(AssembliesType.Player)
#endif
.ToArray();
return playerAssemblyNames;
}
internal static class DefineConstraints {
/// <summary>
/// When define constraints evaluate to false, we need
/// </summary>
/// <param name="defineSymbols"></param>
/// <returns></returns>
/// <remarks>
/// Not aware of a Unity api to read defineConstraints, so we do it ourselves.<br/>
/// Find any asmdef files where the define constraints evaluate to false.
/// </remarks>
public static string[] GetOmittedAssemblies(string[] defineSymbols) {
var guids = AssetDatabase.FindAssets("t:asmdef");
var asmdefFiles = guids.Select(AssetDatabase.GUIDToAssetPath);
var shouldOmit = new List<string>();
foreach (var asmdefFile in asmdefFiles) {
var asmdef = ReadDefineConstraints(asmdefFile);
if (asmdef == null) continue;
if (asmdef.defineConstraints == null || asmdef.defineConstraints.Length == 0) {
// Hot Reload already handles assemblies correctly if they have no define symbols.
continue;
}
var allPass = asmdef.defineConstraints.All(constraint => EvaluateDefineConstraint(constraint, defineSymbols));
if (!allPass) {
shouldOmit.Add(asmdef.name);
}
}
return shouldOmit.ToArray();
}
static AssemblyDefinitionJson ReadDefineConstraints(string path) {
try {
var json = File.ReadAllText(path);
var asmdef = JsonConvert.DeserializeObject<AssemblyDefinitionJson>(json);
return asmdef;
} catch (Exception) {
// ignore malformed asmdef
return null;
}
}
// Unity Define Constraints syntax is described in the docs https://docs.unity3d.com/Manual/class-AssemblyDefinitionImporter.html
static readonly Dictionary<string, string> syntaxMap = new Dictionary<string, string> {
{ "OR", "||" },
{ "AND", "&&" },
{ "NOT", "!" }
};
/// <summary>
/// Evaluate a define constraint like 'UNITY_ANDROID || UNITY_IOS'
/// </summary>
/// <param name="input"></param>
/// <param name="defineSymbols"></param>
/// <returns></returns>
public static bool EvaluateDefineConstraint(string input, string[] defineSymbols) {
// map Unity defineConstraints syntax to DataTable syntax (unity supports both)
foreach (var item in syntaxMap) {
// surround with space because || may not have spaces around it
input = input.Replace(item.Value, $" {item.Key} ");
}
// remove any extra spaces we just created
input = input.Replace(" ", " ");
var tokens = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var token in tokens) {
if (!syntaxMap.ContainsKey(token) && token != "false" && token != "true") {
var index = input.IndexOf(token, StringComparison.Ordinal);
// replace symbols with true or false depending if they are in the array or not.
input = input.Substring(0, index) + defineSymbols.Contains(token) + input.Substring(index + token.Length);
}
}
var dt = new DataTable();
return (bool)dt.Compute(input, "");
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 0b94f2314a044b109de488be1ccd5640
timeCreated: 1674233674
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.17
assetPath: Packages/com.singularitygroup.hotreload/Editor/Helpers/AssemblyOmission.cs
uploadId: 870414
@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor {
struct BuildInfoInput {
public readonly string allDefineSymbols;
public readonly BuildTarget activeBuildTarget;
public readonly string[] omittedProjects;
public readonly bool batchMode;
public readonly string locale;
public BuildInfoInput(string allDefineSymbols, BuildTarget activeBuildTarget, string[] omittedProjects, bool batchMode, string locale) {
this.allDefineSymbols = allDefineSymbols;
this.activeBuildTarget = activeBuildTarget;
this.omittedProjects = omittedProjects;
this.batchMode = batchMode;
this.locale = locale;
}
}
static class BuildInfoHelper {
public static async Task<BuildInfoInput> GetGenerateBuildInfoInput() {
var buildTarget = EditorUserBuildSettings.activeBuildTarget;
var activeDefineSymbols = EditorUserBuildSettings.activeScriptCompilationDefines;
var batchMode = Application.isBatchMode;
var allDefineSymbols = await Task.Run(() => {
return GetAllAndroidMonoBuildDefineSymbolsThreaded(activeDefineSymbols);
});
// cached so unexpensive most of the time
var omittedProjects = AssemblyOmission.GetOmittedProjects(allDefineSymbols);
var locale = PackageConst.DefaultLocale;
return new BuildInfoInput(
allDefineSymbols: allDefineSymbols,
activeBuildTarget: buildTarget,
omittedProjects: omittedProjects,
batchMode: batchMode,
locale: locale
);
}
public static BuildInfo GenerateBuildInfoMainThread() {
return GenerateBuildInfoMainThread(EditorUserBuildSettings.activeBuildTarget);
}
public static BuildInfo GenerateBuildInfoMainThread(BuildTarget buildTarget) {
var allDefineSymbols = GetAllAndroidMonoBuildDefineSymbolsThreaded(EditorUserBuildSettings.activeScriptCompilationDefines);
return GenerateBuildInfoThreaded(new BuildInfoInput(
allDefineSymbols: allDefineSymbols,
activeBuildTarget: buildTarget,
omittedProjects: AssemblyOmission.GetOmittedProjects(allDefineSymbols),
batchMode: Application.isBatchMode,
locale: PackageConst.DefaultLocale
));
}
public static BuildInfo GenerateBuildInfoThreaded(BuildInfoInput input) {
var omittedProjectRegex = String.Join("|", input.omittedProjects.Select(name => Regex.Escape(name)));
var shortCommitHash = GitUtil.GetShortCommitHashOrFallback();
var hostname = IsHumanControllingUs(input.batchMode) ? IpHelper.GetIpAddress() : null;
// Note: add a string to uniquely identify the Unity project. Could use filepath to /MyProject/Assets/ (editor Application.dataPath)
// or application identifier (com.company.appname).
// Do this when supporting multiple projects: SG-28807
// The matching code is in Runtime assembly which compares server response with built BuildInfo.
return new BuildInfo {
projectIdentifier = "SG-29580",
commitHash = shortCommitHash,
defineSymbols = input.allDefineSymbols,
projectOmissionRegex = omittedProjectRegex,
buildMachineHostName = hostname,
buildMachinePort = RequestHelper.port,
activeBuildTarget = input.activeBuildTarget.ToString(),
buildMachineRequestOrigin = RequestHelper.origin,
locale = input.locale
};
}
public static bool IsHumanControllingUs(bool batchMode) {
if (batchMode) {
return false;
}
var isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
return !isCI;
}
private static readonly string[] editorSymbolsToRemove = {
"PLATFORM_ARCH_64",
"UNITY_64",
"UNITY_INCLUDE_TESTS",
"UNITY_EDITOR",
"UNITY_EDITOR_64",
"UNITY_EDITOR_WIN",
"ENABLE_UNITY_COLLECTIONS_CHECKS",
"ENABLE_BURST_AOT",
"RENDER_SOFTWARE_CURSOR",
"PLATFORM_STANDALONE_WIN",
"PLATFORM_STANDALONE",
"UNITY_STANDALONE_WIN",
"UNITY_STANDALONE",
"ENABLE_MOVIES",
"ENABLE_OUT_OF_PROCESS_CRASH_HANDLER",
"ENABLE_WEBSOCKET_HOST",
"ENABLE_CLUSTER_SYNC",
"ENABLE_CLUSTERINPUT",
};
private static readonly string[] androidSymbolsToAdd = {
"CSHARP_7_OR_LATER",
"CSHARP_7_3_OR_NEWER",
"PLATFORM_ANDROID",
"UNITY_ANDROID",
"UNITY_ANDROID_API",
"ENABLE_EGL",
"DEVELOPMENT_BUILD",
"ENABLE_CLOUD_SERVICES_NATIVE_CRASH_REPORTING",
"PLATFORM_SUPPORTS_ADS_ID",
"UNITY_CAN_SHOW_SPLASH_SCREEN",
"UNITY_HAS_GOOGLEVR",
"UNITY_HAS_TANGO",
"ENABLE_SPATIALTRACKING",
"ENABLE_RUNTIME_PERMISSIONS",
"ENABLE_ENGINE_CODE_STRIPPING",
"UNITY_ASTC_ONLY_DECOMPRESS",
"ANDROID_USE_SWAPPY",
"ENABLE_ONSCREEN_KEYBOARD",
"ENABLE_UNITYADS_RUNTIME",
"UNITY_UNITYADS_API",
};
// Currently there is no better way. Alternatively we could hook into unity's call to csc.exe and parse the /define: arguments.
// Hardcoding the differences was less effort and is less error prone.
// I also looked into it and tried all the Build interfaces like this one https://docs.unity3d.com/ScriptReference/Build.IPostBuildPlayerScriptDLLs.html
// and logging EditorUserBuildSettings.activeScriptCompilationDefines in the callbacks - result: all same like editor, so I agree that hardcode is best.
private static string GetAllAndroidMonoBuildDefineSymbolsThreaded(string[] defineSymbols) {
var defines = new HashSet<string>(defineSymbols);
defines.ExceptWith(editorSymbolsToRemove);
defines.UnionWith(androidSymbolsToAdd);
// sort for consistency, must be deterministic
var definesArray = defines.OrderBy(def => def).ToArray();
return String.Join(";", definesArray);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f41ad09ae4f04088bf6c9ad9a4fc0885
timeCreated: 1674220023
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.17
assetPath: Packages/com.singularitygroup.hotreload/Editor/Helpers/BuildInfoHelper.cs
uploadId: 870414
@@ -0,0 +1,106 @@
using System;
using System.Text.RegularExpressions;
using UnityEngine;
using System.Threading.Tasks;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
using SingularityGroup.HotReload.Editor.Localization;
namespace SingularityGroup.HotReload.Editor {
internal static class EditorWindowHelper {
#if UNITY_2020_1_OR_NEWER
public static bool supportsNotifications = true;
#else
public static bool supportsNotifications = false;
#endif
private static readonly Regex ValidEmailRegex = new Regex(@"^(?!\.)(""([^""\r\\]|\\[""\r\\])*""|"
+ @"([-a-z0-9!#$%&'*+/=?^_`{|}~]|(?<!\.)\.)*)(?<!\.)"
+ @"@[a-z0-9][\w\.-]*[a-z0-9]\.[a-z][a-z\.]*[a-z]$", RegexOptions.IgnoreCase);
public static bool IsValidEmailAddress(string email) {
return ValidEmailRegex.IsMatch(email);
}
public static bool IsHumanControllingUs() {
if (Application.isBatchMode) {
// allow for running tests
if (MultiplayerPlaymodeHelper.HasCommandLineArgument(Environment.GetCommandLineArgs(), "-runTests")) {
return true;
}
return false;
}
var isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
return !isCI;
}
internal enum NotificationStatus {
None,
Patching,
NeedsRecompile
}
private static Dictionary<NotificationStatus, GUIContent> notificationContent => new Dictionary<NotificationStatus, GUIContent> {
{ NotificationStatus.Patching, new GUIContent(Translations.Miscellaneous.NotificationPatching)},
{ NotificationStatus.NeedsRecompile, new GUIContent(Translations.Miscellaneous.NotificationNeedsRecompile)},
};
static Type gameViewT;
private static EditorWindow[] gameViewWindows {
get {
gameViewT = gameViewT ?? typeof(EditorWindow).Assembly.GetType("UnityEditor.GameView");
return Resources.FindObjectsOfTypeAll(gameViewT).Cast<EditorWindow>().ToArray();
}
}
private static EditorWindow[] sceneWindows {
get {
return Resources.FindObjectsOfTypeAll(typeof(SceneView)).Cast<EditorWindow>().ToArray();
}
}
private static EditorWindow[] notificationWindows {
get {
return gameViewWindows.Concat(sceneWindows).ToArray();
}
}
static NotificationStatus lastNotificationStatus;
private static DateTime? latestNotificationStartedAt;
private static bool notificationShownRecently => latestNotificationStartedAt != null && DateTime.UtcNow - latestNotificationStartedAt < TimeSpan.FromSeconds(1);
internal static void ShowNotification(NotificationStatus notificationType, float maxDuration = 3) {
// Patch status goes from Unsupported changes to patching rapidly when making unsupported change
// patching also shows right before unsupported changes sometimes
// so we don't override NeedsRecompile notification ever
bool willOverrideNeedsCompileNotification = notificationType != NotificationStatus.NeedsRecompile && notificationShownRecently || lastNotificationStatus == NotificationStatus.NeedsRecompile && notificationShownRecently;
if (!supportsNotifications || willOverrideNeedsCompileNotification) {
return;
}
foreach (EditorWindow notificationWindow in notificationWindows) {
notificationWindow.ShowNotification(notificationContent[notificationType], maxDuration);
notificationWindow.Repaint();
}
latestNotificationStartedAt = DateTime.UtcNow;
lastNotificationStatus = notificationType;
}
internal static void RemoveNotification() {
if (!supportsNotifications) {
return;
}
// only patching notifications should be removed after showing less than 1 second
if (notificationShownRecently && lastNotificationStatus != NotificationStatus.Patching) {
return;
}
foreach (EditorWindow notificationWindow in notificationWindows) {
notificationWindow.RemoveNotification();
notificationWindow.Repaint();
}
latestNotificationStartedAt = null;
lastNotificationStatus = NotificationStatus.None;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: fd463b1f0bfddf34caa662ebe375e5fe
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/Helpers/EditorWindowHelper.cs
uploadId: 870414
@@ -0,0 +1,162 @@
using UnityEngine;
using System.Collections.Generic;
namespace SingularityGroup.HotReload.Editor {
internal enum InvertibleIcon {
BugReport,
Events,
EventsNew,
Recompile,
Logo,
Close,
FoldoutOpen,
FoldoutClosed,
Spinner,
Stop,
Start,
}
internal static class GUIHelper {
private static readonly Dictionary<InvertibleIcon, string> supportedInvertibleIcons = new Dictionary<InvertibleIcon, string> {
{ InvertibleIcon.BugReport, "report_bug" },
{ InvertibleIcon.Events, "events" },
{ InvertibleIcon.Recompile, "refresh" },
{ InvertibleIcon.Logo, "logo" },
{ InvertibleIcon.Close, "close" },
{ InvertibleIcon.FoldoutOpen, "foldout_open" },
{ InvertibleIcon.FoldoutClosed, "foldout_closed" },
{ InvertibleIcon.Spinner, "icon_loading_star_light_mode_96" },
{ InvertibleIcon.Stop, "Icn_Stop" },
{ InvertibleIcon.Start, "Icn_play" },
};
private static readonly Dictionary<InvertibleIcon, Texture2D> invertibleIconCache = new Dictionary<InvertibleIcon, Texture2D>();
private static readonly Dictionary<InvertibleIcon, Texture2D> invertibleIconInvertedCache = new Dictionary<InvertibleIcon, Texture2D>();
private static readonly Dictionary<string, Texture2D> iconCache = new Dictionary<string, Texture2D>();
internal static Texture2D InvertTextureColor(Texture2D originalTexture) {
if (!originalTexture) {
return originalTexture;
}
// Get the original pixels from the texture
Color[] originalPixels = originalTexture.GetPixels();
// Create a new array for the inverted colors
Color[] invertedPixels = new Color[originalPixels.Length];
// Iterate through the pixels and invert the colors while preserving the alpha channel
for (int i = 0; i < originalPixels.Length; i++) {
Color originalColor = originalPixels[i];
Color invertedColor = new Color(1 - originalColor.r, 1 - originalColor.g, 1 - originalColor.b, originalColor.a);
invertedPixels[i] = invertedColor;
}
// Create a new texture and set its pixels
Texture2D invertedTexture = new Texture2D(originalTexture.width, originalTexture.height);
invertedTexture.SetPixels(invertedPixels);
// Apply the changes to the texture
invertedTexture.Apply();
return invertedTexture;
}
internal static Texture2D GetInvertibleIcon(InvertibleIcon invertibleIcon) {
Texture2D iconTexture;
var cache = HotReloadWindowStyles.IsDarkMode ? invertibleIconInvertedCache : invertibleIconCache;
if (!cache.TryGetValue(invertibleIcon, out iconTexture) || !iconTexture) {
var type = invertibleIcon == InvertibleIcon.EventsNew ? InvertibleIcon.Events : invertibleIcon;
iconTexture = Resources.Load<Texture2D>(supportedInvertibleIcons[type]);
// we assume icons are for light mode by default
// therefore if its dark mode we should invert them
if (HotReloadWindowStyles.IsDarkMode) {
iconTexture = InvertTextureColor(iconTexture);
}
cache[type] = iconTexture;
// we combine dot image with Events icon to create a new alert version
if (invertibleIcon == InvertibleIcon.EventsNew) {
var redDot = Resources.Load<Texture2D>("red_dot");
iconTexture = CombineImages(iconTexture, redDot);
cache[InvertibleIcon.EventsNew] = iconTexture;
}
}
return cache[invertibleIcon];
}
internal static Texture2D GetLocalIcon(string iconName) {
Texture2D iconTexture;
if (!iconCache.TryGetValue(iconName, out iconTexture) || !iconTexture) {
iconTexture = Resources.Load<Texture2D>(iconName);
iconCache[iconName] = iconTexture;
}
return iconTexture;
}
static Texture2D CombineImages(Texture2D image1, Texture2D image2) {
if (!image1 || !image2) {
return image1;
}
var combinedImage = new Texture2D(Mathf.Max(image1.width, image2.width), Mathf.Max(image1.height, image2.height));
for (int y = 0; y < combinedImage.height; y++) {
for (int x = 0; x < combinedImage.width; x++) {
Color color1 = x < image1.width && y < image1.height ? image1.GetPixel(x, y) : Color.clear;
Color color2 = x < image2.width && y < image2.height ? image2.GetPixel(x, y) : Color.clear;
combinedImage.SetPixel(x, y, Color.Lerp(color1, color2, color2.a));
}
}
combinedImage.Apply();
return combinedImage;
}
private static readonly Dictionary<Color, Texture2D> textureColorCache = new Dictionary<Color, Texture2D>();
internal static Texture2D ConvertTextureToColor(Color color) {
Texture2D texture;
if (!textureColorCache.TryGetValue(color, out texture) || !texture) {
texture = new Texture2D(1, 1);
texture.SetPixel(0, 0, color);
texture.Apply();
textureColorCache[color] = texture;
}
return texture;
}
private static readonly Dictionary<string, Texture2D> grayTextureCache = new Dictionary<string, Texture2D>();
private static readonly Dictionary<string, Color> colorFactor = new Dictionary<string, Color> {
{ "error", new Color(0.6f, 0.587f, 0.114f) },
};
internal static Texture2D ConvertToGrayscale(string localIcon) {
Texture2D _texture;
if (!grayTextureCache.TryGetValue(localIcon, out _texture) || !_texture) {
var icon = GUIHelper.GetLocalIcon(localIcon);
// Create a copy of the texture
Texture2D copiedTexture = new Texture2D(icon.width, icon.height, TextureFormat.RGBA32, false);
// Convert the copied texture to grayscale
Color[] pixels = icon.GetPixels();
for (int i = 0; i < pixels.Length; i++) {
Color pixel = pixels[i];
Color factor;
if (!colorFactor.TryGetValue(localIcon, out factor)) {
factor = new Color(0.299f, 0.587f, 0.114f);
}
float grayscale = factor.r * pixel.r + factor.g * pixel.g + factor.b * pixel.b;
pixels[i] = new Color(grayscale, grayscale, grayscale, pixel.a); // Preserve alpha channel
}
copiedTexture.SetPixels(pixels);
copiedTexture.Apply();
// Store the grayscale texture in the cache
grayTextureCache[localIcon] = copiedTexture;
return copiedTexture;
}
return _texture;
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: b4be912211814333ab61898b6440dc8e
timeCreated: 1694518358
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.17
assetPath: Packages/com.singularitygroup.hotreload/Editor/Helpers/GUIHelper.cs
uploadId: 870414
@@ -0,0 +1,584 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Localization;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;
using Translations = SingularityGroup.HotReload.Editor.Localization.Translations;
namespace SingularityGroup.HotReload.Editor {
internal static class HotReloadSuggestionsHelper {
internal static void SetSuggestionsShown(HotReloadSuggestionKind hotReloadSuggestionKind) {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
if (EditorPrefs.GetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}")) {
return;
}
EditorPrefs.SetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}", true);
EditorPrefs.SetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}", true);
AlertEntry entry;
if (suggestionMap.TryGetValue(hotReloadSuggestionKind, out entry) && !HotReloadTimelineHelper.Suggestions.Contains(entry)) {
HotReloadTimelineHelper.Suggestions.Insert(0, entry);
HotReloadState.ShowingRedDot = true;
}
}
internal static bool CheckSuggestionActive(HotReloadSuggestionKind hotReloadSuggestionKind) {
return EditorPrefs.GetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}");
}
internal static bool CheckSuggestionShown(HotReloadSuggestionKind hotReloadSuggestionKind) {
return EditorPrefs.GetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}");
}
internal static bool CanShowServerSuggestion(HotReloadSuggestionKind hotReloadSuggestionKind) {
if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerWithSideEffects) {
return !HotReloadState.ShowedFieldInitializerWithSideEffects;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited) {
return !HotReloadState.ShowedFieldInitializerExistingInstancesEdited;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited) {
return !HotReloadState.ShowedFieldInitializerExistingInstancesUnedited;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.AddMonobehaviourMethod) {
return !HotReloadState.ShowedAddMonobehaviourMethods;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.DetailedErrorReportingIsEnabled) {
return !CheckSuggestionShown(HotReloadSuggestionKind.DetailedErrorReportingIsEnabled);
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.UTF8EncodingRequired) {
return true;
}
return false;
}
internal static void SetServerSuggestionShown(HotReloadSuggestionKind hotReloadSuggestionKind) {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
if (hotReloadSuggestionKind == HotReloadSuggestionKind.DetailedErrorReportingIsEnabled) {
HotReloadSuggestionsHelper.SetSuggestionsShown(hotReloadSuggestionKind);
return;
}
if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerWithSideEffects) {
HotReloadState.ShowedFieldInitializerWithSideEffects = true;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited) {
HotReloadState.ShowedFieldInitializerExistingInstancesEdited = true;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited) {
HotReloadState.ShowedFieldInitializerExistingInstancesUnedited = true;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.AddMonobehaviourMethod) {
HotReloadState.ShowedAddMonobehaviourMethods = true;
} else if (hotReloadSuggestionKind == HotReloadSuggestionKind.UTF8EncodingRequired) {
// Allow showing it multiple times
} else {
return;
}
HotReloadSuggestionsHelper.SetSuggestionActive(hotReloadSuggestionKind);
}
// used for cases where suggestion might need to be shown more than once
internal static void SetSuggestionActive(HotReloadSuggestionKind hotReloadSuggestionKind) {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
if (EditorPrefs.GetBool($"HotReloadWindow.SuggestionsShown.{hotReloadSuggestionKind}")) {
return;
}
EditorPrefs.SetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}", true);
AlertEntry entry;
if (suggestionMap.TryGetValue(hotReloadSuggestionKind, out entry) && !HotReloadTimelineHelper.Suggestions.Contains(entry)) {
HotReloadTimelineHelper.Suggestions.Insert(0, entry);
HotReloadState.ShowingRedDot = true;
}
}
internal static void SetSuggestionInactive(HotReloadSuggestionKind hotReloadSuggestionKind) {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
EditorPrefs.SetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}", false);
AlertEntry entry;
if (suggestionMap.TryGetValue(hotReloadSuggestionKind, out entry)) {
HotReloadTimelineHelper.Suggestions.Remove(entry);
}
}
private static void InitSuggestions() {
foreach (HotReloadSuggestionKind value in Enum.GetValues(typeof(HotReloadSuggestionKind))) {
if (!CheckSuggestionActive(value)) {
continue;
}
AlertEntry entry;
if (suggestionMap.TryGetValue(value, out entry) && !HotReloadTimelineHelper.Suggestions.Contains(entry)) {
HotReloadTimelineHelper.Suggestions.Insert(0, entry);
}
}
}
internal static HotReloadSuggestionKind? FindSuggestionKind(AlertEntry targetEntry) {
foreach (KeyValuePair<HotReloadSuggestionKind, AlertEntry> pair in suggestionMap) {
if (pair.Value.Equals(targetEntry)) {
return pair.Key;
}
}
return null;
}
internal static readonly OpenURLButton recompileTroubleshootingButton = new OpenURLButton(Translations.Suggestions.ButtonDocs, Constants.RecompileTroubleshootingURL);
internal static readonly OpenURLButton featuresDocumentationButton = new OpenURLButton(Translations.Suggestions.ButtonDocs, Constants.FeaturesDocumentationURL);
internal static readonly OpenURLButton multipleEditorsDocumentationButton = new OpenURLButton(Translations.Suggestions.ButtonDocs, Constants.MultipleEditorsURL);
internal static readonly OpenURLButton debuggerDocumentationButton = new OpenURLButton(Translations.Suggestions.ButtonMoreInfo, Constants.DebuggerURL);
public static Dictionary<HotReloadSuggestionKind, AlertEntry> suggestionMap = new Dictionary<HotReloadSuggestionKind, AlertEntry> {
{ HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.Award2023Title,
Translations.Suggestions.Award2023Message,
actionData: () => {
GUILayout.Space(6f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonVote)) {
Application.OpenURL(Constants.VoteForAwardURL);
SetSuggestionInactive(HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.UnsupportedChanges, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.UnsupportedChangesTitle,
Translations.Suggestions.UnsupportedChangesMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
featuresDocumentationButton.OnGUI();
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.UnsupportedPackages, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.UnsupportedPackagesTitle,
Translations.Suggestions.UnsupportedPackagesMessage,
iconType: AlertType.UnsupportedChange,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
HotReloadAboutTab.contactButton.OnGUI();
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.AutoRecompiledPlaymodeTitle,
Translations.Suggestions.AutoRecompiledPlaymodeMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
recompileTroubleshootingButton.OnGUI();
GUILayout.Space(5f);
HotReloadAboutTab.discordButton.OnGUI();
GUILayout.Space(5f);
HotReloadAboutTab.contactButton.OnGUI();
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
#if UNITY_2022_1_OR_NEWER
{ HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.AutoRecompiled2022Title,
Translations.Suggestions.AutoRecompiled2022Message,
iconType: AlertType.UnsupportedChange,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonUseBuildTimeOnlyAtlas)) {
if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2) {
EditorSettings.spritePackerMode = SpritePackerMode.SpriteAtlasV2Build;
} else {
EditorSettings.spritePackerMode = SpritePackerMode.BuildTimeOnlyAtlas;
}
}
if (GUILayout.Button(Translations.Suggestions.ButtonOpenSettings)) {
SettingsService.OpenProjectSettings("Project/Editor");
}
if (GUILayout.Button(Translations.Suggestions.ButtonIgnoreSuggestion)) {
SetSuggestionInactive(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
hasExitButton: false
)},
#endif
{ HotReloadSuggestionKind.MultidimensionalArrays, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.MultidimensionalArraysTitle,
Translations.Suggestions.MultidimensionalArraysMessage,
iconType: AlertType.UnsupportedChange,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonLearnMore)) {
string url;
if (PackageConst.DefaultLocaleField == Locale.SimplifiedChinese) {
url = "https://learn.microsoft.com/zh-cn/dotnet/fundamentals/code-analysis/quality-rules/ca1814";
} else {
url = "https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1814";
}
Application.OpenURL(url);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.EditorsWithoutHRRunning, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.EditorsWithoutHRTitle,
Translations.Suggestions.EditorsWithoutHRMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonStopHotReload)) {
EditorCodePatcher.StopCodePatcher().Forget();
}
GUILayout.Space(5f);
multipleEditorsDocumentationButton.OnGUI();
GUILayout.Space(5f);
if (GUILayout.Button(Translations.Suggestions.ButtonDontShowAgain)) {
HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.EditorsWithoutHRRunning);
HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
}
GUILayout.FlexibleSpace();
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.UnsupportedChange
)},
// Not in use (never reported from the server)
{ HotReloadSuggestionKind.FieldInitializerWithSideEffects, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.FieldInitializerSideEffectsTitle,
Translations.Suggestions.FieldInitializerSideEffectsMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonOK)) {
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(Translations.Suggestions.ButtonDontShowAgain)) {
SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
}
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.DetailedErrorReportingIsEnabled, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.DetailedErrorReportingTitle,
Translations.Suggestions.DetailedErrorReportingMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
GUILayout.Space(4f);
if (GUILayout.Button(Translations.Suggestions.ButtonOKPadded)) {
SetSuggestionInactive(HotReloadSuggestionKind.DetailedErrorReportingIsEnabled);
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(Translations.Suggestions.ButtonDisable)) {
HotReloadSettingsTab.DisableDetailedErrorReportingInner(true);
SetSuggestionInactive(HotReloadSuggestionKind.DetailedErrorReportingIsEnabled);
}
GUILayout.Space(10f);
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
// Not in use (never reported from the server)
{ HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.FieldInitializerEditedTitle,
Translations.Suggestions.FieldInitializerEditedMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonTurnOff)) {
#pragma warning disable CS0618
HotReloadSettingsTab.ApplyApplyFieldInitializerEditsToExistingClassInstances(false);
#pragma warning restore CS0618
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
}
if (GUILayout.Button(Translations.Suggestions.ButtonOpenSettings)) {
HotReloadWindow.Current.SelectTab(typeof(HotReloadSettingsTab));
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(Translations.Suggestions.ButtonDontShowAgain)) {
SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
}
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.FieldInitializerUneditedTitle,
Translations.Suggestions.FieldInitializerUneditedMessage,
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonOK)) {
SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited);
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.AddMonobehaviourMethod, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.AddMonobehaviourMethodTitle,
Translations.Suggestions.AddMonobehaviourMethodMessage,
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonOK)) {
SetSuggestionInactive(HotReloadSuggestionKind.AddMonobehaviourMethod);
}
if (GUILayout.Button(Translations.Suggestions.ButtonAutoRecompile)) {
SetSuggestionInactive(HotReloadSuggestionKind.AddMonobehaviourMethod);
HotReloadPrefs.AutoRecompilePartiallyUnsupportedChanges = true;
HotReloadPrefs.DisplayNewMonobehaviourMethodsAsPartiallySupported = true;
HotReloadRunTab.RecompileWithChecks();
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(Translations.Suggestions.ButtonDontShowAgain)) {
SetSuggestionsShown(HotReloadSuggestionKind.AddMonobehaviourMethod);
SetSuggestionInactive(HotReloadSuggestionKind.AddMonobehaviourMethod);
}
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
#if UNITY_2020_1_OR_NEWER
{ HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.SwitchToDebugModeTitle,
Translations.Suggestions.SwitchToDebugModeMessage,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonSwitchToDebugMode) && HotReloadRunTab.ConfirmExitPlaymode(Translations.Suggestions.SwitchToDebugModeConfirmation)) {
HotReloadRunTab.SwitchToDebugMode();
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.UnsupportedChange
)},
#endif
{ HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.DebuggerAttachedTitle,
Translations.Suggestions.DebuggerAttachedMessagePaused,
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonKeepEnabledDuringDebugging)) {
SetSuggestionInactive(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
HotReloadPrefs.AutoDisableHotReloadWithDebugger = false;
}
GUILayout.FlexibleSpace();
debuggerDocumentationButton.OnGUI();
if (GUILayout.Button(Translations.Suggestions.ButtonDontShowAgain)) {
SetSuggestionsShown(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
SetSuggestionInactive(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
}
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.DebuggerMethodsTitle,
Translations.Suggestions.DebuggerMethodsMessage,
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonRecompile)) {
SetSuggestionInactive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached);
if (HotReloadRunTab.ConfirmExitPlaymode(Translations.Suggestions.DebuggerMethodsConfirmation)) {
HotReloadRunTab.Recompile();
}
}
GUILayout.FlexibleSpace();
debuggerDocumentationButton.OnGUI();
GUILayout.Space(8f);
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.UnsupportedChange,
hasExitButton: false
)},
{ HotReloadSuggestionKind.UTF8EncodingRequired, new AlertEntry(
AlertType.Suggestion,
Translations.Suggestions.UTF8EncodingRequiredTitle,
Translations.Suggestions.UTF8EncodingRequiredMessage,
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(Translations.Suggestions.ButtonOK)) {
SetSuggestionInactive(HotReloadSuggestionKind.UTF8EncodingRequired);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.UnsupportedChange,
hasExitButton: false
)},
};
static ListRequest listRequest;
static string[] unsupportedPackages = new[] {
"com.unity.entities",
"com.firstgeargames.fishnet",
};
static List<string> unsupportedPackagesList;
static DateTime lastPlaymodeChange;
public static void Init() {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
listRequest = Client.List(offlineMode: false, includeIndirectDependencies: true);
EditorApplication.playModeStateChanged += state => {
lastPlaymodeChange = DateTime.UtcNow;
};
CompilationPipeline.compilationStarted += obj => {
if (DateTime.UtcNow - lastPlaymodeChange < TimeSpan.FromSeconds(1) && !HotReloadState.RecompiledUnsupportedChangesOnExitPlaymode) {
#if UNITY_2022_1_OR_NEWER
SetSuggestionsShown(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
#else
SetSuggestionsShown(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges);
#endif
}
HotReloadState.RecompiledUnsupportedChangesOnExitPlaymode = false;
};
InitSuggestions();
}
private static DateTime lastCheckedUnityInstances = DateTime.UtcNow;
public static void Check() {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
if (listRequest.IsCompleted &&
unsupportedPackagesList == null)
{
unsupportedPackagesList = new List<string>();
if (listRequest.Result != null) {
foreach (var packageInfo in listRequest.Result) {
if (unsupportedPackages.Contains(packageInfo.name)) {
unsupportedPackagesList.Add(packageInfo.name);
}
}
}
if (unsupportedPackagesList.Count > 0) {
SetSuggestionsShown(HotReloadSuggestionKind.UnsupportedPackages);
}
}
CheckEditorsWithoutHR();
#if UNITY_2022_1_OR_NEWER
if (EditorSettings.spritePackerMode == SpritePackerMode.AlwaysOnAtlas || EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2) {
SetSuggestionsShown(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
} else if (CheckSuggestionActive(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022)) {
SetSuggestionInactive(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
EditorPrefs.SetBool($"HotReloadWindow.SuggestionsShown.{HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022}", false);
}
#endif
}
private static void CheckEditorsWithoutHR() {
if (!ServerHealthCheck.I.IsServerHealthy) {
HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
return;
}
if (checkingEditorsWihtoutHR ||
(DateTime.UtcNow - lastCheckedUnityInstances).TotalSeconds < 5)
{
return;
}
CheckEditorsWithoutHRAsync().Forget();
}
static bool checkingEditorsWihtoutHR;
private static async Task CheckEditorsWithoutHRAsync() {
try {
checkingEditorsWihtoutHR = true;
var editorsWithoutHr = await RequestHelper.RequestEditorsWithoutHRRunning();
if (editorsWithoutHr == null) {
return;
}
var showSuggestion = editorsWithoutHr.editorsWithoutHRRunning;
if (!showSuggestion) {
HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
return;
}
if (!HotReloadState.ShowedEditorsWithoutHR && ServerHealthCheck.I.IsServerHealthy) {
HotReloadSuggestionsHelper.SetSuggestionActive(HotReloadSuggestionKind.EditorsWithoutHRRunning);
}
} finally {
checkingEditorsWihtoutHR = false;
lastCheckedUnityInstances = DateTime.UtcNow;
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 9cc471e812b143599ef5dde1d7ec022a
timeCreated: 1694632601
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.17
assetPath: Packages/com.singularitygroup.hotreload/Editor/Helpers/HotReloadSuggestionsHelper.cs
uploadId: 870414
@@ -0,0 +1,608 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using JetBrains.Annotations;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Localization;
using SingularityGroup.HotReload.Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
using Translations = SingularityGroup.HotReload.Editor.Localization.Translations;
namespace SingularityGroup.HotReload.Editor {
internal enum TimelineType {
Suggestions,
Timeline,
}
internal enum AlertType {
Suggestion,
UnsupportedChange,
CompileError,
PartiallySupportedChange,
AppliedChange,
UndetectedChange,
}
internal enum AlertEntryType {
Error,
Failure,
InlinedMethod,
PatchApplied,
PartiallySupportedChange,
UndetectedChange,
}
internal enum EntryType {
Parent,
Child,
Standalone,
Foldout,
}
internal class PersistedAlertData {
public readonly AlertData[] alertDatas;
public PersistedAlertData(AlertData[] alertDatas) {
this.alertDatas = alertDatas;
}
}
internal class AlertData {
public readonly AlertEntryType alertEntryType;
public readonly string errorString;
public readonly string methodName;
public readonly string methodSimpleName;
public readonly PartiallySupportedChange partiallySupportedChange;
public readonly EntryType entryType;
public readonly bool detiled;
public readonly DateTime createdAt;
public readonly string[] patchedMembersDisplayNames;
public AlertData(AlertEntryType alertEntryType, DateTime createdAt, bool detiled = false, EntryType entryType = EntryType.Standalone, string errorString = null, string methodName = null, string methodSimpleName = null, PartiallySupportedChange partiallySupportedChange = default(PartiallySupportedChange), string[] patchedMembersDisplayNames = null) {
this.alertEntryType = alertEntryType;
this.createdAt = createdAt;
this.detiled = detiled;
this.entryType = entryType;
this.errorString = errorString;
this.methodName = methodName;
this.methodSimpleName = methodSimpleName;
this.partiallySupportedChange = partiallySupportedChange;
this.patchedMembersDisplayNames = patchedMembersDisplayNames;
}
}
internal class AlertEntry {
internal readonly AlertType alertType;
internal readonly string title;
internal readonly DateTime timestamp;
internal readonly string description;
[CanBeNull] internal readonly Action actionData;
internal readonly AlertType iconType;
internal readonly string shortDescription;
internal readonly EntryType entryType;
internal readonly AlertData alertData;
internal readonly bool hasExitButton;
internal AlertEntry(AlertType alertType, string title, string description, DateTime timestamp, string shortDescription = null, Action actionData = null, AlertType? iconType = null, EntryType entryType = EntryType.Standalone, AlertData alertData = default(AlertData), bool hasExitButton = true) {
this.alertType = alertType;
this.title = title;
this.description = description;
this.shortDescription = shortDescription;
this.actionData = actionData;
this.iconType = iconType ?? alertType;
this.timestamp = timestamp;
this.entryType = entryType;
this.alertData = alertData;
this.hasExitButton = hasExitButton;
}
}
internal static class HotReloadTimelineHelper {
internal const int maxVisibleEntries = 40;
private static List<AlertEntry> eventsTimeline = new List<AlertEntry>();
internal static List<AlertEntry> EventsTimeline => eventsTimeline;
static readonly string filePath = Path.Combine(PackageConst.LibraryCachePath, "eventEntries.json");
public static async Task InitPersistedEvents() {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
if (!File.Exists(filePath)) {
return;
}
var redDotShown = HotReloadState.ShowingRedDot;
try {
var persistedAlertData = await Task.Run(() => JsonConvert.DeserializeObject<PersistedAlertData>(File.ReadAllText(filePath)));
eventsTimeline = new List<AlertEntry>(persistedAlertData.alertDatas.Length);
for (int i = persistedAlertData.alertDatas.Length - 1; i >= 0; i--) {
AlertData alertData = persistedAlertData.alertDatas[i];
switch (alertData.alertEntryType) {
case AlertEntryType.Error:
CreateErrorEventEntry(errorString: alertData.errorString, entryType: alertData.entryType, createdAt: alertData.createdAt);
break;
#if UNITY_2020_1_OR_NEWER
case AlertEntryType.InlinedMethod:
CreateInlinedMethodsEntry(alertData.patchedMembersDisplayNames, alertData.entryType, alertData.createdAt);
break;
#endif
case AlertEntryType.Failure:
if (alertData.entryType == EntryType.Parent) {
CreateReloadFinishedWithWarningsEventEntry(createdAt: alertData.createdAt, patchedMembersDisplayNames: alertData.patchedMembersDisplayNames);
} else {
CreatePatchFailureEventEntry(errorString: alertData.errorString, methodName: alertData.methodName, methodSimpleName: alertData.methodSimpleName, entryType: alertData.entryType, createdAt: alertData.createdAt);
}
break;
case AlertEntryType.PatchApplied:
CreateReloadFinishedEventEntry(
createdAt: alertData.createdAt,
patchedMethodsDisplayNames: alertData.patchedMembersDisplayNames
);
break;
case AlertEntryType.PartiallySupportedChange:
if (alertData.entryType == EntryType.Parent) {
CreateReloadPartiallyAppliedEventEntry(createdAt: alertData.createdAt, patchedMethodsDisplayNames: alertData.patchedMembersDisplayNames);
} else {
CreatePartiallyAppliedEventEntry(alertData.partiallySupportedChange, entryType: alertData.entryType, detailed: alertData.detiled, createdAt: alertData.createdAt);
}
break;
case AlertEntryType.UndetectedChange:
CreateReloadUndetectedChangeEventEntry(createdAt: alertData.createdAt);
break;
}
}
} catch (Exception e) {
Log.Warning(Translations.Errors.WarningInitializingEventEntries, e);
} finally {
// Ensure red dot is not triggered for existing entries
HotReloadState.ShowingRedDot = redDotShown;
}
}
internal static async Task PersistTimeline() {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
var persistedData = new PersistedAlertData(eventsTimeline.Where(x => x.alertType != AlertType.CompileError).Select(x => x.alertData).ToArray());
try {
await Task.Run(() => File.WriteAllText(path: filePath, contents: JsonConvert.SerializeObject(persistedData)));
} catch (Exception e) {
Log.Warning(Translations.Errors.WarningPersistingEventEntries, e);
}
}
internal static void ClearPersistance() {
if (MultiplayerPlaymodeHelper.IsClone) {
return;
}
Task.Run(() => File.Delete(filePath));
eventsTimeline = new List<AlertEntry>();
}
internal static readonly Dictionary<AlertType, string> alertIconString = new Dictionary<AlertType, string> {
{ AlertType.Suggestion, "alert_info" },
{ AlertType.UnsupportedChange, "warning" },
{ AlertType.CompileError, "error" },
{ AlertType.PartiallySupportedChange, "infos" },
{ AlertType.AppliedChange, "applied_patch" },
{ AlertType.UndetectedChange, "undetected" },
};
#pragma warning disable CS0612 // obsolete
public static Dictionary<PartiallySupportedChange, string> partiallySupportedChangeDescriptions => new Dictionary<PartiallySupportedChange, string> {
{PartiallySupportedChange.LambdaClosure, Translations.Timeline.PartiallySupportedLambdaClosure},
{PartiallySupportedChange.EditAsyncMethod, Translations.Timeline.PartiallySupportedEditAsyncMethod},
{PartiallySupportedChange.AddMonobehaviourMethod, Translations.Timeline.PartiallySupportedAddMonobehaviourMethod},
{PartiallySupportedChange.EditMonobehaviourField, Translations.Timeline.PartiallySupportedEditMonobehaviourField},
{PartiallySupportedChange.EditCoroutine, Translations.Timeline.PartiallySupportedEditCoroutine},
{PartiallySupportedChange.EditGenericFieldInitializer, Translations.Timeline.PartiallySupportedEditGenericFieldInitializer},
{PartiallySupportedChange.AddEnumMember, Translations.Timeline.PartiallySupportedAddEnumMember},
{PartiallySupportedChange.EditFieldInitializer, Translations.Timeline.PartiallySupportedEditFieldInitializer},
{PartiallySupportedChange.AddMethodWithAttributes, Translations.Timeline.PartiallySupportedAddMethodWithAttributes},
{PartiallySupportedChange.AddFieldWithAttributes, Translations.Timeline.PartiallySupportedAddFieldWithAttributes},
{PartiallySupportedChange.GenericMethodInGenericClass, Translations.Timeline.PartiallySupportedGenericMethodInGenericClass},
{PartiallySupportedChange.NewCustomSerializableField, Translations.Timeline.PartiallySupportedNewCustomSerializableField},
{PartiallySupportedChange.MultipleFieldsEditedInTheSameType, Translations.Timeline.PartiallySupportedMultipleFieldsEditedInTheSameType},
};
#pragma warning restore CS0612
internal static List<AlertEntry> Suggestions = new List<AlertEntry>();
internal static int UnsupportedChangesCount => EventsTimeline.Count(alert => alert.alertType == AlertType.UnsupportedChange && alert.entryType != EntryType.Child);
internal static int PartiallySupportedChangesCount => EventsTimeline.Count(alert => alert.alertType == AlertType.PartiallySupportedChange && alert.entryType != EntryType.Child);
internal static int UndetectedChangesCount => EventsTimeline.Count(alert => alert.alertType == AlertType.UndetectedChange && alert.entryType != EntryType.Child);
internal static int CompileErrorsCount => EventsTimeline.Count(alert => alert.alertType == AlertType.CompileError);
internal static int AppliedChangesCount => EventsTimeline.Count(alert => alert.alertType == AlertType.AppliedChange);
static Regex shortDescriptionRegex = new Regex(PackageConst.DefaultLocale == Locale.SimplifiedChinese ? @"^([\p{L}\p{N}_]+)\s([\p{L}\p{N}_]+)(?=:)" : @"^(\w+)\s(\w+)(?=:)", RegexOptions.Compiled);
internal static int GetRunTabTimelineEventCount() {
int total = 0;
if (HotReloadPrefs.RunTabUnsupportedChangesFilter) {
total += UnsupportedChangesCount;
}
if (HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter) {
total += PartiallySupportedChangesCount;
}
if (HotReloadPrefs.RunTabUndetectedPatchesFilter) {
total += UndetectedChangesCount;
}
if (HotReloadPrefs.RunTabCompileErrorFilter) {
total += CompileErrorsCount;
}
if (HotReloadPrefs.RunTabAppliedPatchesFilter) {
total += AppliedChangesCount;
}
return total;
}
internal static List<AlertEntry> expandedEntries = new List<AlertEntry>();
internal static void RenderCompileButton() {
if (GUILayout.Button(Translations.Common.ButtonRecompile.Trim(), GUILayout.Width(80))) {
HotReloadRunTab.RecompileWithChecks();
}
}
private static float maxScrollPos;
internal static void RenderErrorEventActions(string description, ErrorData errorData) {
int maxLen = 2400;
string text = errorData.stacktrace;
if (text.Length > maxLen) {
text = text.Substring(0, maxLen) + "...";
}
GUILayout.TextArea(text, HotReloadWindowStyles.StacktraceTextAreaStyle);
if (errorData.file || !errorData.stacktrace.Contains("error CS")) {
GUILayout.Space(10f);
}
using (new EditorGUILayout.HorizontalScope()) {
if (!errorData.stacktrace.Contains("error CS")) {
RenderCompileButton();
}
// Link
if (errorData.file) {
GUILayout.FlexibleSpace();
if (GUILayout.Button(errorData.linkString, HotReloadWindowStyles.LinkStyle)) {
AssetDatabase.OpenAsset(errorData.file, Math.Max(errorData.lineNumber, 1));
}
}
}
}
private static Texture2D GetFilterIcon(int count, AlertType alertType) {
if (count == 0) {
return GUIHelper.ConvertToGrayscale(alertIconString[alertType]);
}
return GUIHelper.GetLocalIcon(alertIconString[alertType]);
}
internal static void RenderAlertFilters() {
using (new EditorGUILayout.HorizontalScope()) {
var text = AppliedChangesCount > 999 ? "999+" : " " + AppliedChangesCount;
HotReloadPrefs.RunTabAppliedPatchesFilter = GUILayout.Toggle(
HotReloadPrefs.RunTabAppliedPatchesFilter,
new GUIContent(text, GetFilterIcon(AppliedChangesCount, AlertType.AppliedChange)),
HotReloadWindowStyles.EventFiltersStyle);
GUILayout.Space(-1f);
text = UndetectedChangesCount > 999 ? "999+" : " " + UndetectedChangesCount;
HotReloadPrefs.RunTabUndetectedPatchesFilter = GUILayout.Toggle(
HotReloadPrefs.RunTabUndetectedPatchesFilter,
new GUIContent(text, GetFilterIcon(UnsupportedChangesCount, AlertType.UndetectedChange)),
HotReloadWindowStyles.EventFiltersStyle);
GUILayout.Space(-1f);
text = PartiallySupportedChangesCount > 999 ? "999+" : " " + PartiallySupportedChangesCount;
HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter = GUILayout.Toggle(
HotReloadPrefs.RunTabPartiallyAppliedPatchesFilter,
new GUIContent(text, GetFilterIcon(PartiallySupportedChangesCount, AlertType.PartiallySupportedChange)),
HotReloadWindowStyles.EventFiltersStyle);
GUILayout.Space(-1f);
text = UnsupportedChangesCount > 999 ? "999+" : " " + UnsupportedChangesCount;
HotReloadPrefs.RunTabUnsupportedChangesFilter = GUILayout.Toggle(
HotReloadPrefs.RunTabUnsupportedChangesFilter,
new GUIContent(text, GetFilterIcon(UnsupportedChangesCount, AlertType.UnsupportedChange)),
HotReloadWindowStyles.EventFiltersStyle);
GUILayout.Space(-1f);
text = CompileErrorsCount > 999 ? "999+" : " " + CompileErrorsCount;
HotReloadPrefs.RunTabCompileErrorFilter = GUILayout.Toggle(
HotReloadPrefs.RunTabCompileErrorFilter,
new GUIContent(text, GetFilterIcon(CompileErrorsCount, AlertType.CompileError)),
HotReloadWindowStyles.EventFiltersStyle);
}
}
internal static void CreateErrorEventEntry(string errorString, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
var timestamp = createdAt ?? DateTime.Now;
var alertType = errorString.Contains("error CS")
? AlertType.CompileError
: AlertType.UnsupportedChange;
var title = errorString.Contains("error CS")
? Translations.Utility.CompileError
: Translations.Utility.UnsupportedChange;
ErrorData errorData = ErrorData.GetErrorData(errorString);
var description = errorData.error;
string shortDescription = null;
if (alertType != AlertType.CompileError) {
shortDescription = shortDescriptionRegex.Match(description).Value;
}
Action actionData = () => RenderErrorEventActions(description, errorData);
InsertEntry(new AlertEntry(
timestamp: timestamp,
alertType: alertType,
title: title,
description: description,
shortDescription: shortDescription,
actionData: actionData,
entryType: entryType,
alertData: new AlertData(AlertEntryType.Error, createdAt: timestamp, errorString: errorString, entryType: entryType)
));
}
#if UNITY_2020_1_OR_NEWER
internal static void CreateInlinedMethodsEntry(string[] patchedMethodsDisplayNames, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
var truncated = false;
if (patchedMethodsDisplayNames?.Length > 25) {
patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
truncated = true;
}
var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
var timestamp = createdAt ?? DateTime.Now;
var entry = new AlertEntry(
timestamp: timestamp,
alertType : AlertType.UnsupportedChange,
title: Translations.Timeline.EventTitleFailedApplyingPatch,
description: $"{Translations.Timeline.EventDescriptionInlinedMethods}\n\n• {(truncated ? patchesList + "\n..." : patchesList)}",
entryType: EntryType.Parent,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
RenderCompileButton();
var suggestion = HotReloadSuggestionsHelper.suggestionMap[HotReloadSuggestionKind.SwitchToDebugModeForInlinedMethods];
if (suggestion?.actionData != null) {
suggestion.actionData();
}
}
},
alertData: new AlertData(AlertEntryType.InlinedMethod, createdAt: timestamp, patchedMembersDisplayNames: patchedMethodsDisplayNames, entryType: EntryType.Parent)
);
InsertEntry(entry);
if (patchedMethodsDisplayNames?.Length > 0) {
expandedEntries.Add(entry);
}
}
#endif
internal static void CreatePatchFailureEventEntry(string errorString, string methodName, string methodSimpleName = null, EntryType entryType = EntryType.Standalone, DateTime? createdAt = null) {
var timestamp = createdAt ?? DateTime.Now;
ErrorData errorData = ErrorData.GetErrorData(errorString);
var title = Translations.Timeline.EventTitleFailedApplyingPatch;
Action actionData = () => RenderErrorEventActions(errorData.error, errorData);
InsertEntry(new AlertEntry(
timestamp: timestamp,
alertType : AlertType.UnsupportedChange,
title: title,
description: string.Format(Translations.Timeline.EventDescriptionFailedApplyingPatchTapForMore, title, methodName),
shortDescription: methodSimpleName,
actionData: actionData,
entryType: entryType,
alertData: new AlertData(AlertEntryType.Failure, createdAt: timestamp, errorString: errorString, methodName: methodName, methodSimpleName: methodSimpleName, entryType: entryType)
));
}
public static T[] TruncateList<T>(T[] originalList, int len) {
if (originalList.Length <= len) {
return originalList;
}
// Create a new list with a maximum of 25 items
T[] truncatedList = new T[len];
for (int i = 0; i < originalList.Length && i < len; i++) {
truncatedList[i] = originalList[i];
}
return truncatedList;
}
internal static void CreateReloadFinishedEventEntry(DateTime? createdAt = null, string[] patchedMethodsDisplayNames = null) {
var truncated = false;
if (patchedMethodsDisplayNames?.Length > 25) {
patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
truncated = true;
}
var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
var timestamp = createdAt ?? DateTime.Now;
var entry = new AlertEntry(
timestamp: timestamp,
alertType: AlertType.AppliedChange,
title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Reloaded],
description: patchedMethodsDisplayNames?.Length > 0
? $"• {(truncated ? patchesList + "\n..." : patchesList)}"
: Translations.Timeline.EventDescriptionNoIssuesFound,
entryType: patchedMethodsDisplayNames?.Length > 0 ? EntryType.Parent : EntryType.Standalone,
alertData: new AlertData(
AlertEntryType.PatchApplied,
createdAt: timestamp,
entryType: EntryType.Standalone,
patchedMembersDisplayNames: patchedMethodsDisplayNames)
);
InsertEntry(entry);
if (patchedMethodsDisplayNames?.Length > 0) {
expandedEntries.Add(entry);
}
}
internal static void CreateReloadFinishedWithWarningsEventEntry(DateTime? createdAt = null, string[] patchedMembersDisplayNames = null) {
var truncated = false;
if (patchedMembersDisplayNames?.Length > 25) {
patchedMembersDisplayNames = TruncateList(patchedMembersDisplayNames, 25);
truncated = true;
}
var patchesList = patchedMembersDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMembersDisplayNames) : "";
var timestamp = createdAt ?? DateTime.Now;
var entry = new AlertEntry(
timestamp: timestamp,
alertType: AlertType.UnsupportedChange,
title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Unsupported],
description: patchedMembersDisplayNames?.Length > 0 ? $"• {(truncated ? patchesList + "\n...\n\n" + Translations.Timeline.EventDescriptionSeeUnsupportedChangesBelow : patchesList + "\n\n" + Translations.Timeline.EventDescriptionSeeUnsupportedChangesBelow)}" : Translations.Timeline.EventDescriptionSeeDetailedEntriesBelow,
entryType: EntryType.Parent,
alertData: new AlertData(AlertEntryType.Failure, createdAt: timestamp, entryType: EntryType.Parent, patchedMembersDisplayNames: patchedMembersDisplayNames)
);
InsertEntry(entry);
if (patchedMembersDisplayNames?.Length > 0) {
expandedEntries.Add(entry);
}
}
internal static void CreateReloadPartiallyAppliedEventEntry(DateTime? createdAt = null, string[] patchedMethodsDisplayNames = null) {
var truncated = false;
if (patchedMethodsDisplayNames?.Length > 25) {
patchedMethodsDisplayNames = TruncateList(patchedMethodsDisplayNames, 25);
truncated = true;
}
var patchesList = patchedMethodsDisplayNames?.Length > 0 ? string.Join("\n• ", patchedMethodsDisplayNames) : "";
var timestamp = createdAt ?? DateTime.Now;
var entry = new AlertEntry(
timestamp: timestamp,
alertType: AlertType.PartiallySupportedChange,
title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.PartiallySupported],
description: patchedMethodsDisplayNames?.Length > 0 ? $"• {(truncated ? patchesList + "\n...\n\n" + Translations.Timeline.EventDescriptionSeePartiallyAppliedChangesBelow : patchesList + "\n\n" + Translations.Timeline.EventDescriptionSeePartiallyAppliedChangesBelow)}" : Translations.Timeline.EventDescriptionSeeDetailedEntriesBelow,
entryType: EntryType.Parent,
alertData: new AlertData(AlertEntryType.PartiallySupportedChange, createdAt: timestamp, entryType: EntryType.Parent, patchedMembersDisplayNames: patchedMethodsDisplayNames)
);
InsertEntry(entry);
if (patchedMethodsDisplayNames?.Length > 0) {
expandedEntries.Add(entry);
}
}
internal static void CreateReloadUndetectedChangeEventEntry(DateTime? createdAt = null) {
var timestamp = createdAt ?? DateTime.Now;
InsertEntry(new AlertEntry(
timestamp: timestamp,
alertType : AlertType.UndetectedChange,
title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Undetected],
description: Translations.Timeline.EventDescriptionUndetectedChange,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
RenderCompileButton();
GUILayout.FlexibleSpace();
OpenURLButton.Render(Translations.Suggestions.ButtonDocs, Constants.UndetectedChangesURL);
GUILayout.Space(10f);
}
},
entryType: EntryType.Foldout,
alertData: new AlertData(AlertEntryType.UndetectedChange, createdAt: timestamp, entryType: EntryType.Parent)
));
}
internal static void CreatePartiallyAppliedEventEntry(PartiallySupportedChange partiallySupportedChange, EntryType entryType = EntryType.Standalone, bool detailed = true, DateTime? createdAt = null) {
var timestamp = createdAt ?? DateTime.Now;
string description;
if (!partiallySupportedChangeDescriptions.TryGetValue(partiallySupportedChange, out description)) {
return;
}
InsertEntry(new AlertEntry(
timestamp: timestamp,
alertType : AlertType.PartiallySupportedChange,
title : detailed ? Translations.Timeline.EventTitleChangePartiallyApplied : ToString(partiallySupportedChange),
description : description,
shortDescription: detailed ? ToString(partiallySupportedChange) : null,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
RenderCompileButton();
GUILayout.FlexibleSpace();
if (GetPartiallySupportedChangePref(partiallySupportedChange)) {
if (GUILayout.Button(Translations.Timeline.ButtonIgnoreEventType, HotReloadWindowStyles.LinkStyle)) {
HidePartiallySupportedChange(partiallySupportedChange);
HotReloadRunTab.RepaintInstant();
}
}
}
},
entryType: entryType,
alertData: new AlertData(AlertEntryType.PartiallySupportedChange, createdAt: timestamp, partiallySupportedChange: partiallySupportedChange, entryType: entryType, detiled: detailed)
));
}
internal static void InsertEntry(AlertEntry entry) {
eventsTimeline.Insert(0, entry);
if (entry.alertType != AlertType.AppliedChange) {
HotReloadState.ShowingRedDot = true;
}
}
internal static void ClearEntries() {
eventsTimeline.Clear();
}
internal static bool GetPartiallySupportedChangePref(PartiallySupportedChange key) {
return EditorPrefs.GetBool($"HotReloadWindow.ShowPartiallySupportedChangeType.{key}", true);
}
internal static void HidePartiallySupportedChange(PartiallySupportedChange key) {
EditorPrefs.SetBool($"HotReloadWindow.ShowPartiallySupportedChangeType.{key}", false);
// loop over scroll entries to remove hidden entries
for (var i = EventsTimeline.Count - 1; i >= 0; i--) {
var eventEntry = EventsTimeline[i];
if (eventEntry.alertData.partiallySupportedChange == key) {
EventsTimeline.Remove(eventEntry);
}
}
}
// performance optimization (Enum.ToString uses reflection)
internal static string ToString(this PartiallySupportedChange change) {
#pragma warning disable CS0612 // obsolete
switch (change) {
case PartiallySupportedChange.LambdaClosure:
return nameof(PartiallySupportedChange.LambdaClosure);
case PartiallySupportedChange.EditAsyncMethod:
return nameof(PartiallySupportedChange.EditAsyncMethod);
case PartiallySupportedChange.AddMonobehaviourMethod:
return nameof(PartiallySupportedChange.AddMonobehaviourMethod);
case PartiallySupportedChange.EditMonobehaviourField:
return nameof(PartiallySupportedChange.EditMonobehaviourField);
case PartiallySupportedChange.EditCoroutine:
return nameof(PartiallySupportedChange.EditCoroutine);
case PartiallySupportedChange.EditGenericFieldInitializer:
return nameof(PartiallySupportedChange.EditGenericFieldInitializer);
case PartiallySupportedChange.AddEnumMember:
return nameof(PartiallySupportedChange.AddEnumMember);
case PartiallySupportedChange.EditFieldInitializer:
return nameof(PartiallySupportedChange.EditFieldInitializer);
case PartiallySupportedChange.AddMethodWithAttributes:
return nameof(PartiallySupportedChange.AddMethodWithAttributes);
case PartiallySupportedChange.GenericMethodInGenericClass:
return nameof(PartiallySupportedChange.GenericMethodInGenericClass);
case PartiallySupportedChange.AddFieldWithAttributes:
return nameof(PartiallySupportedChange.AddFieldWithAttributes);
case PartiallySupportedChange.NewCustomSerializableField:
return nameof(PartiallySupportedChange.NewCustomSerializableField);
case PartiallySupportedChange.MultipleFieldsEditedInTheSameType:
return nameof(PartiallySupportedChange.MultipleFieldsEditedInTheSameType);
#pragma warning restore CS0612
default:
throw new ArgumentOutOfRangeException(nameof(change), change, null);
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: ffb65be71b8b4d14800f8b28bf68d0ab
timeCreated: 1695210350
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.17
assetPath: Packages/com.singularitygroup.hotreload/Editor/Helpers/HotReloadTimelineHelper.cs
uploadId: 870414
@@ -0,0 +1,80 @@
using System;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor {
internal class Spinner {
internal static string SpinnerIconPath => "icon_loading_star_light_mode_96";
internal static Texture2D spinnerTexture => GUIHelper.GetInvertibleIcon(InvertibleIcon.Spinner);
private Texture2D _rotatedTextureLight;
private Texture2D _rotatedTextureDark;
private Texture2D rotatedTextureLight => _rotatedTextureLight ? _rotatedTextureLight : _rotatedTextureLight = GetCopy(spinnerTexture);
private Texture2D rotatedTextureDark => _rotatedTextureDark ? _rotatedTextureDark : _rotatedTextureDark = GetCopy(spinnerTexture);
internal Texture2D rotatedTexture => HotReloadWindowStyles.IsDarkMode ? rotatedTextureDark : rotatedTextureLight;
private float _rotationAngle;
private DateTime _lastRotation;
private int _rotationPeriod;
internal Spinner(int rotationPeriodInMilliseconds) {
_rotationPeriod = rotationPeriodInMilliseconds;
}
internal Texture2D GetIcon() {
if (DateTime.UtcNow - _lastRotation > TimeSpan.FromMilliseconds(_rotationPeriod)) {
_lastRotation = DateTime.UtcNow;
_rotationAngle += 45;
if (_rotationAngle >= 360f)
_rotationAngle -= 360f;
return RotateImage(spinnerTexture, _rotationAngle);
}
return rotatedTexture;
}
private Texture2D RotateImage(Texture2D originalTexture, float angle) {
int w = originalTexture.width;
int h = originalTexture.height;
int x, y;
float centerX = w / 2f;
float centerY = h / 2f;
for (x = 0; x < w; x++) {
for (y = 0; y < h; y++) {
float dx = x - centerX;
float dy = y - centerY;
float distance = Mathf.Sqrt(dx * dx + dy * dy);
float oldAngle = Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
float newAngle = oldAngle + angle;
float newX = centerX + distance * Mathf.Cos(newAngle * Mathf.Deg2Rad);
float newY = centerY + distance * Mathf.Sin(newAngle * Mathf.Deg2Rad);
if (newX >= 0 && newX < w && newY >= 0 && newY < h) {
rotatedTexture.SetPixel(x, y, originalTexture.GetPixel((int)newX, (int)newY));
} else {
rotatedTexture.SetPixel(x, y, Color.clear);
}
}
}
rotatedTexture.Apply();
return rotatedTexture;
}
public static Texture2D GetCopy(Texture2D tex, TextureFormat format = TextureFormat.RGBA32, bool mipChain = false) {
var tmp = RenderTexture.GetTemporary(tex.width, tex.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear);
Graphics.Blit(tex, tmp);
RenderTexture.active = tmp;
try {
var copy = new Texture2D(tex.width, tex.height, format, mipChain: mipChain);
copy.ReadPixels(new Rect(0, 0, tmp.width, tmp.height), 0, 0);
copy.Apply();
return copy;
} finally {
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(tmp);
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 8bd77f0465824c5da3e1454f75c6e93c
timeCreated: 1685871830
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.17
assetPath: Packages/com.singularitygroup.hotreload/Editor/Helpers/Spinner.cs
uploadId: 870414
@@ -0,0 +1,95 @@
using UnityEngine;
using System.Reflection;
using System;
using System.Collections;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("SingularityGroup.HotReload.Demo")]
namespace SingularityGroup.HotReload.Editor {
internal class UnitySettingsHelper {
public static UnitySettingsHelper I = new UnitySettingsHelper();
private bool initialized;
private object pref;
private PropertyInfo prefColorProp;
private MethodInfo setMethod;
private Type settingsType;
private Type prefColorType;
const string currentPlaymodeTintPrefKey = "Playmode tint";
internal bool playmodeTintSupported => EditorCodePatcher.config.changePlaymodeTint && EnsureInitialized();
private UnitySettingsHelper() {
EnsureInitialized();
}
private bool EnsureInitialized() {
if (initialized) {
return true;
}
try {
// cache members for performance
settingsType = settingsType ?? (settingsType = typeof(UnityEditor.Editor).Assembly.GetType($"UnityEditor.PrefSettings"));
prefColorType = prefColorType ?? (prefColorType = typeof(UnityEditor.Editor).Assembly.GetType($"UnityEditor.PrefColor"));
prefColorProp = prefColorProp ?? (prefColorProp = prefColorType?.GetProperty("Color", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public));
pref = pref ?? (pref = GetPref(settingsType: settingsType, prefColorType: prefColorType));
setMethod = setMethod ?? (setMethod = GetSetMethod(settingsType: settingsType, prefColorType: prefColorType));
if (prefColorProp == null
|| pref == null
|| setMethod == null
) {
return false;
}
// clear cache for performance
settingsType = null;
prefColorType = null;
initialized = true;
return true;
} catch {
return false;
}
}
private static MethodInfo GetSetMethod(Type settingsType, Type prefColorType) {
var setMethodBase = settingsType?.GetMethod("Set", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
return setMethodBase?.MakeGenericMethod(prefColorType);
}
private static object GetPref(Type settingsType, Type prefColorType) {
var prefsMethodBase = settingsType?.GetMethod("Prefs", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
var prefsMethod = prefsMethodBase?.MakeGenericMethod(prefColorType);
var prefs = (IEnumerable)prefsMethod?.Invoke(null, Array.Empty<object>());
if (prefs != null) {
foreach (object kvp in prefs) {
var key = kvp.GetType().GetProperty("Key", BindingFlags.Instance | BindingFlags.Public)?.GetMethod.Invoke(kvp, Array.Empty<object>());
if (key?.ToString() == currentPlaymodeTintPrefKey) {
return kvp.GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public)?.GetMethod.Invoke(kvp, Array.Empty<object>());
}
}
}
return null;
}
public Color? GetCurrentPlaymodeColor() {
if (!playmodeTintSupported) {
return null;
}
return (Color)prefColorProp.GetValue(pref);
}
public void SetPlaymodeTint(Color color) {
if (!playmodeTintSupported) {
return;
}
prefColorProp.SetValue(pref, color);
setMethod.Invoke(null, new object[] { currentPlaymodeTintPrefKey, pref });
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 34fb1222dc00466ab4e3db7383bd00ee
timeCreated: 1694279476
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.17
assetPath: Packages/com.singularitygroup.hotreload/Editor/Helpers/UnitySettingsHelper.cs
uploadId: 870414