feat: add hotreload

This commit is contained in:
2025-09-07 14:18:42 +08:00
parent 3a1063beaf
commit 2c3d87361b
379 changed files with 38335 additions and 0 deletions
@@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using UnityEditor;
using System.Linq;
using System.Runtime.CompilerServices;
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("To compile C# files same as a Player build, we must omit projects which aren't part of the selected Player build.");
var omitted = GetOmittedProjects(EditorUserBuildSettings.activeScriptCompilationDefines);
Log.Info("---------");
foreach (var name in omitted) {
Log.Info("omitted editor/other project named: {0}", 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("found project named {0}", name);
}
foreach (var playerAssemblyName in playerAssemblies) {
Log.Debug("player assembly named {0}", 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,3 @@
fileFormatVersion: 2
guid: 0b94f2314a044b109de488be1ccd5640
timeCreated: 1674233674
@@ -0,0 +1,144 @@
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 BuildInfoInput(string allDefineSymbols, BuildTarget activeBuildTarget, string[] omittedProjects, bool batchMode) {
this.allDefineSymbols = allDefineSymbols;
this.activeBuildTarget = activeBuildTarget;
this.omittedProjects = omittedProjects;
this.batchMode = batchMode;
}
}
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);
return new BuildInfoInput(
allDefineSymbols: allDefineSymbols,
activeBuildTarget: buildTarget,
omittedProjects: omittedProjects,
batchMode: batchMode
);
}
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
));
}
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,
};
}
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.
public 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,3 @@
fileFormatVersion: 2
guid: f41ad09ae4f04088bf6c9ad9a4fc0885
timeCreated: 1674220023
@@ -0,0 +1,101 @@
using System;
using System.Text.RegularExpressions;
using UnityEngine;
using System.Threading.Tasks;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
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) {
return false;
}
var isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
return !isCI;
}
internal enum NotificationStatus {
None,
Patching,
NeedsRecompile
}
private static readonly Dictionary<NotificationStatus, GUIContent> notificationContent = new Dictionary<NotificationStatus, GUIContent> {
{ NotificationStatus.Patching, new GUIContent("[Hot Reload] Applying patches...")},
{ NotificationStatus.NeedsRecompile, new GUIContent("[Hot Reload] Unsupported Changes detected! Recompiling...")},
};
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,11 @@
fileFormatVersion: 2
guid: fd463b1f0bfddf34caa662ebe375e5fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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,3 @@
fileFormatVersion: 2
guid: b4be912211814333ab61898b6440dc8e
timeCreated: 1694518358
@@ -0,0 +1,538 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using SingularityGroup.HotReload.DTO;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;
namespace SingularityGroup.HotReload.Editor {
internal static class HotReloadSuggestionsHelper {
internal static void SetSuggestionsShown(HotReloadSuggestionKind hotReloadSuggestionKind) {
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);
}
return false;
}
internal static void SetServerSuggestionShown(HotReloadSuggestionKind hotReloadSuggestionKind) {
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 {
return;
}
HotReloadSuggestionsHelper.SetSuggestionActive(hotReloadSuggestionKind);
}
// used for cases where suggestion might need to be shown more than once
internal static void SetSuggestionActive(HotReloadSuggestionKind hotReloadSuggestionKind) {
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) {
EditorPrefs.SetBool($"HotReloadWindow.SuggestionsActive.{hotReloadSuggestionKind}", false);
AlertEntry entry;
if (suggestionMap.TryGetValue(hotReloadSuggestionKind, out entry)) {
HotReloadTimelineHelper.Suggestions.Remove(entry);
}
}
internal 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("Docs", Constants.RecompileTroubleshootingURL);
internal static readonly OpenURLButton featuresDocumentationButton = new OpenURLButton("Docs", Constants.FeaturesDocumentationURL);
internal static readonly OpenURLButton multipleEditorsDocumentationButton = new OpenURLButton("Docs", Constants.MultipleEditorsURL);
internal static readonly OpenURLButton debuggerDocumentationButton = new OpenURLButton("More Info", Constants.DebuggerURL);
public static Dictionary<HotReloadSuggestionKind, AlertEntry> suggestionMap = new Dictionary<HotReloadSuggestionKind, AlertEntry> {
{ HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023, new AlertEntry(
AlertType.Suggestion,
"Vote for the \"Best Development Tool\" Award!",
"Hot Reload was nominated for the \"Best Development Tool\" Award. Please consider voting. Thank you!",
actionData: () => {
GUILayout.Space(6f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(" Vote ")) {
Application.OpenURL(Constants.VoteForAwardURL);
SetSuggestionInactive(HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.UnsupportedChanges, new AlertEntry(
AlertType.Suggestion,
"Which changes does Hot Reload support?",
"Hot Reload supports most code changes, but there are some limitations. Generally, changes to methods and fields are supported. Things like adding new types is not (yet) supported. See the documentation for the list of current features and our current roadmap",
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
featuresDocumentationButton.OnGUI();
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.UnsupportedPackages, new AlertEntry(
AlertType.Suggestion,
"Unsupported package detected",
"The following packages are only partially supported: ECS, Mirror, Fishnet, and Photon. Hot Reload will work in the project, but changes specific to those packages might not hot-reload",
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,
"Unity recompiles on enter/exit play mode?",
"If you have an issue with the Unity Editor recompiling when the Play Mode state changes, more info is available in the docs. Feel free to reach out if you require assistance. We'll be glad to help.",
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,
"Unsupported setting detected",
"The 'Sprite Packer Mode' setting can cause unintended recompilations if set to 'Sprite Atlas V1 - Always Enabled'",
iconType: AlertType.UnsupportedChange,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(" Use \"Build Time Only Atlas\" ")) {
if (EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2) {
EditorSettings.spritePackerMode = SpritePackerMode.SpriteAtlasV2Build;
} else {
EditorSettings.spritePackerMode = SpritePackerMode.BuildTimeOnlyAtlas;
}
}
if (GUILayout.Button(" Open Settings ")) {
SettingsService.OpenProjectSettings("Project/Editor");
}
if (GUILayout.Button(" Ignore suggestion ")) {
SetSuggestionInactive(HotReloadSuggestionKind.AutoRecompiledWhenPlaymodeStateChanges2022);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
hasExitButton: false
)},
#endif
{ HotReloadSuggestionKind.MultidimensionalArrays, new AlertEntry(
AlertType.Suggestion,
"Use jagged instead of multidimensional arrays",
"Hot Reload doesn't support methods with multidimensional arrays ([,]). You can work around this by using jagged arrays ([][])",
iconType: AlertType.UnsupportedChange,
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(" Learn more ")) {
Application.OpenURL("https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1814");
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout
)},
{ HotReloadSuggestionKind.EditorsWithoutHRRunning, new AlertEntry(
AlertType.Suggestion,
"Some Unity instances don't have Hot Reload running.",
"Make sure that either: \n1) Hot Reload is installed and running on all Editor instances, or \n2) Hot Reload is stopped in all Editor instances where it is installed.",
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(" Stop Hot Reload ")) {
EditorCodePatcher.StopCodePatcher().Forget();
}
GUILayout.Space(5f);
multipleEditorsDocumentationButton.OnGUI();
GUILayout.Space(5f);
if (GUILayout.Button(" Don't show again ")) {
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,
"Field initializer with side-effects detected",
"A field initializer update might have side effects, e.g. calling a method or creating an object.\n\nWhile Hot Reload does support this, it can sometimes be confusing when the initializer logic runs at 'unexpected times'.",
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(" OK ")) {
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(" Don't show again ")) {
SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerWithSideEffects);
}
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.DetailedErrorReportingIsEnabled, new AlertEntry(
AlertType.Suggestion,
"Detailed error reporting is enabled",
"When an error happens in Hot Reload, the exception stacktrace is sent as telemetry to help diagnose and fix the issue.\nThe exception stack trace is only included if it originated from the Hot Reload package or binary. Stacktraces from your own code are not sent.\nYou can disable detailed error reporting to prevent telemetry from including any information about your project.",
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
GUILayout.Space(4f);
if (GUILayout.Button(" OK ")) {
SetSuggestionInactive(HotReloadSuggestionKind.DetailedErrorReportingIsEnabled);
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(" Disable ")) {
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,
"Field initializer edit updated the value of existing class instances",
"By default, Hot Reload updates field values of existing object instances when new field initializer has constant value.\n\nIf you want to change this behavior, disable the \"Apply field initializer edits to existing class instances\" option in Settings or click the button below.",
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(" Turn off ")) {
#pragma warning disable CS0618
HotReloadSettingsTab.ApplyApplyFieldInitializerEditsToExistingClassInstances(false);
#pragma warning restore CS0618
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
}
if (GUILayout.Button(" Open Settings ")) {
HotReloadWindow.Current.SelectTab(typeof(HotReloadSettingsTab));
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(" Don't show again ")) {
SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesEdited);
}
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited, new AlertEntry(
AlertType.Suggestion,
"Field initializer edits don't apply to existing objects",
"By default, Hot Reload applies field initializer edits of existing fields only to new objects (newly instantiated classes), just like normal C#.\n\nFor rapid prototyping, you can use static fields which will update across all instances.",
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(" OK ")) {
SetSuggestionsShown(HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited);
SetSuggestionInactive(HotReloadSuggestionKind.FieldInitializerExistingInstancesUnedited);
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.AddMonobehaviourMethod, new AlertEntry(
AlertType.Suggestion,
"New MonoBehaviour methods are not shown in the inspector",
"New methods in MonoBehaviours are not shown in the inspector until the script is recompiled. This is a limitation of Hot Reload handling of Unity's serialization system.\n\nYou can use the button below to auto recompile partially supported changes such as this one.",
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(" OK ")) {
SetSuggestionInactive(HotReloadSuggestionKind.AddMonobehaviourMethod);
}
if (GUILayout.Button(" Auto Recompile ")) {
SetSuggestionInactive(HotReloadSuggestionKind.AddMonobehaviourMethod);
HotReloadPrefs.AutoRecompilePartiallyUnsupportedChanges = true;
HotReloadPrefs.DisplayNewMonobehaviourMethodsAsPartiallySupported = true;
HotReloadRunTab.RecompileWithChecks();
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(" Don't show again ")) {
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,
"Switch code optimization to Debug Mode",
"In Release Mode some methods are inlined, which prevents Hot Reload from applying changes. A clear warning is always shown when this happens, but you can use Debug Mode to avoid the issue altogether",
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(" Switch to Debug mode ") && HotReloadRunTab.ConfirmExitPlaymode("Switching code optimization will stop Play Mode.\n\nDo you wish to proceed?")) {
HotReloadRunTab.SwitchToDebugMode();
}
GUILayout.FlexibleSpace();
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.UnsupportedChange
)},
#endif
{ HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached, new AlertEntry(
AlertType.Suggestion,
"Hot Reload is disabled while a debugger is attached",
"Hot Reload automatically disables itself while a debugger is attached, as it can otherwise interfere with certain debugger features.\nWhile disabled, every code change will trigger a full Unity recompilation.\n\nYou can choose to keep Hot Reload enabled while a debugger is attached, though some features like debugger variable inspection might not always work as expected.",
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(" Keep enabled during debugging ")) {
SetSuggestionInactive(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
HotReloadPrefs.AutoDisableHotReloadWithDebugger = false;
}
GUILayout.FlexibleSpace();
debuggerDocumentationButton.OnGUI();
if (GUILayout.Button(" Don't show again ")) {
SetSuggestionsShown(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
SetSuggestionInactive(HotReloadSuggestionKind.HotReloadWhileDebuggerIsAttached);
}
}
},
timestamp: DateTime.Now,
entryType: EntryType.Foldout,
iconType: AlertType.Suggestion
)},
{ HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached, new AlertEntry(
AlertType.Suggestion,
"Hot Reload may interfere with your debugger session",
"Some debugger features, like variable inspection, might not work as expected for methods patched during the Hot Reload session. A full Unity recompile is required to get the full debugger experience.",
actionData: () => {
GUILayout.Space(8f);
using (new EditorGUILayout.HorizontalScope()) {
if (GUILayout.Button(" Recompile ")) {
SetSuggestionInactive(HotReloadSuggestionKind.HotReloadedMethodsWhenDebuggerIsAttached);
if (HotReloadRunTab.ConfirmExitPlaymode("Using the Recompile button will stop Play Mode.\n\nDo you wish to proceed?")) {
HotReloadRunTab.Recompile();
}
}
GUILayout.FlexibleSpace();
debuggerDocumentationButton.OnGUI();
GUILayout.Space(8f);
}
},
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() {
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 (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);
HotReloadState.ShowedEditorsWithoutHR = true;
}
} finally {
checkingEditorsWihtoutHR = false;
lastCheckedUnityInstances = DateTime.UtcNow;
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9cc471e812b143599ef5dde1d7ec022a
timeCreated: 1694632601
@@ -0,0 +1,606 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
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 void InitPersistedEvents() {
if (!File.Exists(filePath)) {
return;
}
var redDotShown = HotReloadState.ShowingRedDot;
try {
var persistedAlertData = 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($"Failed initializing Hot Reload event entries on start: {e}");
} finally {
// Ensure red dot is not triggered for existing entries
HotReloadState.ShowingRedDot = redDotShown;
}
}
internal static void PersistTimeline() {
var alertDatas = new AlertData[eventsTimeline.Count];
for (var i = 0; i < eventsTimeline.Count; i++) {
alertDatas[i] = eventsTimeline[i].alertData;
}
var persistedData = new PersistedAlertData(alertDatas);
try {
File.WriteAllText(path: filePath, contents: JsonConvert.SerializeObject(persistedData));
} catch (Exception e) {
Log.Warning($"Failed persisting Hot Reload event entries: {e}");
}
}
internal static void ClearPersistance() {
try {
File.Delete(filePath);
} catch {
// ignore
}
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, "A lambda closure was edited (captured variable was added or removed). Changes to it will only be visible to the next created lambda(s)."},
{PartiallySupportedChange.EditAsyncMethod, "An async method was edited. Changes to it will only be visible the next time this method is called."},
{PartiallySupportedChange.AddMonobehaviourMethod, "A new method was added or made public. It will not show up in the Inspector until the next full recompilation."},
{PartiallySupportedChange.EditMonobehaviourField, "A field in a MonoBehaviour was removed or reordered. The inspector will not notice this change until the next full recompilation."},
{PartiallySupportedChange.EditCoroutine, "An IEnumerator/IEnumerable was edited. When used as a coroutine, changes to it will only be visible the next time the coroutine is created."},
{PartiallySupportedChange.EditGenericFieldInitializer, "A field initializer inside generic class was edited. Field initializer will not have any effect until the next full recompilation."},
{PartiallySupportedChange.AddEnumMember, "An enum member was added. ToString and other reflection methods work only after the next full recompilation. Additionally, changes to the enum order may not apply until you patch usages in other places of the code."},
{PartiallySupportedChange.EditFieldInitializer, "A field initializer was edited. Changes will only apply to new instances of that type, since the initializer for an object only runs when it is created."},
{PartiallySupportedChange.AddMethodWithAttributes, "A method with attributes was added. Method attributes will not have any effect until the next full recompilation."},
{PartiallySupportedChange.AddFieldWithAttributes, "A field with attributes was added. Field attributes will not have any effect until the next full recompilation."},
{PartiallySupportedChange.GenericMethodInGenericClass, "A generic method was edited. Usages in non-generic classes applied, but usages in the generic classes are not supported."},
{PartiallySupportedChange.NewCustomSerializableField, "A new custom serializable field was added. The inspector will not notice this change until the next full recompilation."},
{PartiallySupportedChange.MultipleFieldsEditedInTheSameType, "Multiple fields modified in the same type during a single patch. Their values have been reset."},
};
#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(@"^(\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("Recompile", 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")
? "Compile error"
: "Unsupported change";
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: "Failed applying patch to method",
description: $"Some methods got inlined by the Unity compiler and cannot be patched by Hot Reload. Switch to Debug mode to avoid this problem.\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 = $"Failed applying patch to method";
Action actionData = () => RenderErrorEventActions(errorData.error, errorData);
InsertEntry(new AlertEntry(
timestamp: timestamp,
alertType : AlertType.UnsupportedChange,
title: title,
description: $"{title}: {methodName}, tap here to see more.",
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)}"
: "No issues found",
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\nSee unsupported changes below" : patchesList + "\n\nSee unsupported changes below")}" : "See detailed entries below",
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\nSee partially applied changes below" : patchesList + "\n\nSee partially applied changes below")}" : "See detailed entries below",
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: "Code semantics didn't change (e.g. whitespace) or the change requires manual recompile.\n\n" +
"Recompile to force-apply changes.",
actionData: () => {
GUILayout.Space(10f);
using (new EditorGUILayout.HorizontalScope()) {
RenderCompileButton();
GUILayout.FlexibleSpace();
OpenURLButton.Render("Docs", 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 ? "Change partially applied" : 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("Ignore this event type ", 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,3 @@
fileFormatVersion: 2
guid: ffb65be71b8b4d14800f8b28bf68d0ab
timeCreated: 1695210350
@@ -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,3 @@
fileFormatVersion: 2
guid: 8bd77f0465824c5da3e1454f75c6e93c
timeCreated: 1685871830
@@ -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,3 @@
fileFormatVersion: 2
guid: 34fb1222dc00466ab4e3db7383bd00ee
timeCreated: 1694279476