first commit
This commit is contained in:
@@ -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,328 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Compilation;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEditor.PackageManager.Requests;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SingularityGroup.HotReload.Editor {
|
||||
|
||||
public enum HotReloadSuggestionKind {
|
||||
UnsupportedChanges,
|
||||
UnsupportedPackages,
|
||||
[Obsolete] SymbolicLinks,
|
||||
AutoRecompiledWhenPlaymodeStateChanges,
|
||||
UnityBestDevelopmentToolAward2023,
|
||||
#if UNITY_2022_1_OR_NEWER
|
||||
AutoRecompiledWhenPlaymodeStateChanges2022,
|
||||
#endif
|
||||
MultidimensionalArrays,
|
||||
EditorsWithoutHRRunning,
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
// 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);
|
||||
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 the method definition and body are allowed. Non-method changes (like adding/editing classes and fields) are not 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 work. Contact us if these packages are a big part of your project",
|
||||
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, please consult the documentation, and don’t hesitate to reach out to us if you need assistance",
|
||||
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 \"Sprite Atlas V2\" ")) {
|
||||
EditorSettings.spritePackerMode = SpritePackerMode.SpriteAtlasV2;
|
||||
}
|
||||
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 multidimensional ([,]) arrays. Jagged arrays ([][]) are a better alternative, and Microsoft recommends using them instead",
|
||||
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
|
||||
)},
|
||||
};
|
||||
|
||||
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>();
|
||||
var packages = listRequest.Result;
|
||||
foreach (var packageInfo in packages) {
|
||||
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) {
|
||||
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 showSuggestion = await Task.Run(() => {
|
||||
try {
|
||||
var runningUnities = Process.GetProcessesByName("Unity").Length;
|
||||
var runningPatchers = Process.GetProcessesByName("CodePatcherCLI").Length;
|
||||
return runningPatchers > 0 && runningUnities > runningPatchers;
|
||||
} catch (ArgumentException) {
|
||||
// On some devices GetProcessesByName throws ArgumentException for no good reason.
|
||||
// it happens rarely and the feature is not the most important so proper solution is not required
|
||||
return false;
|
||||
}
|
||||
});
|
||||
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,550 @@
|
||||
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,
|
||||
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[] patchedMethodsDisplayNames;
|
||||
|
||||
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[] patchedMethodsDisplayNames = 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.patchedMethodsDisplayNames = patchedMethodsDisplayNames;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
case AlertEntryType.Failure:
|
||||
if (alertData.entryType == EntryType.Parent) {
|
||||
CreateReloadFinishedWithWarningsEventEntry(createdAt: alertData.createdAt, patchedMethodsDisplayNames: alertData.patchedMethodsDisplayNames);
|
||||
} 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.patchedMethodsDisplayNames
|
||||
);
|
||||
break;
|
||||
case AlertEntryType.PartiallySupportedChange:
|
||||
if (alertData.entryType == EntryType.Parent) {
|
||||
CreateReloadPartiallyAppliedEventEntry(createdAt: alertData.createdAt, patchedMethodsDisplayNames: alertData.patchedMethodsDisplayNames);
|
||||
} 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" },
|
||||
};
|
||||
|
||||
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. 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.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.GenericMethodInGenericClass, "A generic method was edited. Usages in non-generic classes applied, but usages in the generic classes are not supported."},
|
||||
};
|
||||
|
||||
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)
|
||||
));
|
||||
}
|
||||
|
||||
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,
|
||||
patchedMethodsDisplayNames: patchedMethodsDisplayNames)
|
||||
);
|
||||
|
||||
InsertEntry(entry);
|
||||
if (patchedMethodsDisplayNames?.Length > 0) {
|
||||
expandedEntries.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void CreateReloadFinishedWithWarningsEventEntry(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.UnsupportedChange,
|
||||
title: EditorIndicationState.IndicationText[EditorIndicationState.IndicationStatus.Unsupported],
|
||||
description: patchedMethodsDisplayNames?.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, patchedMethodsDisplayNames: patchedMethodsDisplayNames)
|
||||
);
|
||||
InsertEntry(entry);
|
||||
if (patchedMethodsDisplayNames?.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, patchedMethodsDisplayNames: 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) {
|
||||
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.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);
|
||||
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
|
||||
Reference in New Issue
Block a user