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