using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Text; using System.Text.RegularExpressions; using UnityEditor; using UnityEditor.PackageManager; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.Rendering; using static UnityEngine.GUILayout; [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("PrimeTween.Internal")] namespace PrimeTween { internal class PrimeTweenInstaller : ScriptableObject { [SerializeField] internal SceneAsset demoScene; [SerializeField] internal SceneAsset demoSceneUrp; [SerializeField] internal Color uninstallButtonColor; [ContextMenu(nameof(ResetReviewRequest))] void ResetReviewRequest() => ReviewRequest.ResetReviewRequest(); [ContextMenu(nameof(DebugReviewRequest))] void DebugReviewRequest() => ReviewRequest.DebugReviewRequest(); } [CustomEditor(typeof(PrimeTweenInstaller), false)] internal class InstallerInspector : Editor { internal const string pluginName = "PrimeTween"; internal const string pluginPackageId = "com.kyrylokuzyk.primetween"; internal const string tgzPath = "Assets/Plugins/PrimeTween/internal/com.kyrylokuzyk.primetween.tgz"; internal const string newTgzPath = "Assets/Plugins/PrimeTween/internal/com.kyrylokuzyk.primetween-" + version + ".tgz"; const string documentationUrl = "https://github.com/KyryloKuzyk/PrimeTween"; bool isInstalled; bool hasNewTgz; GUIStyle boldButtonStyle; GUIStyle uninstallButtonStyle; GUIStyle wordWrapLabelStyle; void OnEnable() { isInstalled = CheckPluginInstalled(); hasNewTgz = File.Exists(newTgzPath); } /// Use Package Manager because Unity 2018 doesn't support version defines static bool CheckPluginInstalled() { var listRequest = Client.List(true); while (!listRequest.IsCompleted) { } return listRequest.Result.Any(_ => _.name == pluginPackageId); } public override void OnInspectorGUI() { if (boldButtonStyle == null) { boldButtonStyle = new GUIStyle(GUI.skin.button) { fontStyle = FontStyle.Bold }; } var installer = (PrimeTweenInstaller)target; if (uninstallButtonStyle == null) { uninstallButtonStyle = new GUIStyle(GUI.skin.button) { normal = { textColor = installer.uninstallButtonColor } }; } if (wordWrapLabelStyle == null) { wordWrapLabelStyle = new GUIStyle(GUI.skin.label) { wordWrap = true, richText = true, margin = new RectOffset(4, 4, 8, 8) }; } EditorGUI.indentLevel = 5; Space(8); Label(pluginName, EditorStyles.boldLabel); Space(4); if (!isInstalled) { if (Button("Install " + pluginName)) { installPlugin(); } return; } if (hasNewTgz) { if (Button($"Update to {version}", boldButtonStyle)) { ReviewRequest.OnPackageUpdate(); } Space(8); } if (Button("Documentation", boldButtonStyle)) { Application.OpenURL(documentationUrl); } Space(8); if (Button("Open Demo", boldButtonStyle)) { var rpAsset = GraphicsSettings. #if UNITY_2019_3_OR_NEWER defaultRenderPipeline; #else renderPipelineAsset; #endif bool isUrp = rpAsset != null && rpAsset.GetType().Name.Contains("Universal"); var demoScene = isUrp ? installer.demoSceneUrp : installer.demoScene; if (demoScene == null) { Debug.LogError("Please re-import the plugin from Asset Store and import the 'Demo' folder.\n"); return; } var path = AssetDatabase.GetAssetPath(demoScene); EditorSceneManager.OpenScene(path); } #if UNITY_2019_4_OR_NEWER if (Button("Import Basic Examples")) { EditorUtility.DisplayDialog(pluginName, $"Please select the '{pluginName}' package in 'Package Manager', then press the 'Samples/Import' button at the bottom of the plugin's description.", "Ok"); UnityEditor.PackageManager.UI.Window.Open(pluginPackageId); } #endif if (Button("Support")) { Application.OpenURL("https://github.com/KyryloKuzyk/PrimeTween#support"); } Space(8); if (Button("Uninstall", uninstallButtonStyle)) { Client.Remove(pluginPackageId); isInstalled = false; var msg = $"Please remove the folder manually to uninstall {pluginName} completely: 'Assets/Plugins/{pluginName}'"; EditorUtility.DisplayDialog(pluginName, msg, "Ok"); Debug.Log(msg); } if (EditorPrefs.GetBool(InsertCallbackBug.showInsertCallbackBugUi, false)) { Space(24); Label("Updating from PrimeTween [1.1.10 - 1.1.22]", EditorStyles.boldLabel); Label("The behaviour of 'Sequence.ChainCallback()' and 'InsertCallback()' was fixed in PrimeTween 1.2.0 so the code written with older versions may work differently in some cases.", wordWrapLabelStyle); if (Button("Find potential issues")) { InsertCallbackBug.Find(); } BeginHorizontal(); if (Button("More info")) { Application.OpenURL(InsertCallbackBug.moreInfoUrl); } if (Button("Download version 1.1.22")) { Application.OpenURL("https://github.com/KyryloKuzyk/PrimeTween/blob/545dcc52769d52841e282c772e98c8984bfeb243/Benchmarks/Packages/com.kyrylokuzyk.primetween.tgz"); } EndHorizontal(); } Space(24); Label("Enjoying PrimeTween?", EditorStyles.boldLabel); Label("Consider leaving an honest review and starring PrimeTween on GitHub!\n\n" + "Honest reviews make PrimeTween better and help other developers discover it.", wordWrapLabelStyle); if (Button("Leave review!", GUI.skin.button)) { ReviewRequest.DisableReviewRequest(); ReviewRequest.OpenReviewsURL(); } } static void installPlugin() { if (File.Exists(newTgzPath)) { MoveAndRenameTgzArchive(); } ReviewRequest.OnBeforeInstall(); var path = $"file:../{tgzPath}"; var addRequest = Client.Add(path); while (!addRequest.IsCompleted) { } if (addRequest.Status == StatusCode.Success) { Debug.Log($"{pluginName} installed successfully.\n" + $"Offline documentation is located at Packages/{pluginName}/Documentation.md.\n" + $"Online documentation: {documentationUrl}\n"); } else { Debug.LogError($"Please re-import the plugin from the Asset Store and check that the file exists: [{path}].\n\n{addRequest.Error?.message}\n"); } } internal static void MoveAndRenameTgzArchive() { Assert.IsTrue(File.Exists(newTgzPath)); Assert.IsTrue(File.Exists(newTgzPath + ".meta")); File.Delete(tgzPath); File.Delete(tgzPath + ".meta"); File.Move(newTgzPath, tgzPath); File.Move(newTgzPath + ".meta", tgzPath + ".meta"); RevertTgzMeta(); } static void RevertTgzMeta() { const string path = tgzPath + ".meta"; Assert.IsTrue(File.Exists(path), path); File.WriteAllText(path, @"fileFormatVersion: 2 guid: cdd0c4b9889044d73bc958a922ada300 DefaultImporter: externalObjects: {} userData: assetBundleName: assetBundleVariant: "); } [InitializeOnLoadMethod] static void InitOnLoad() { AssetDatabase.importPackageCompleted += name => { if (name.Contains(pluginName)) { if (CheckPluginInstalled()) { ReviewRequest.OnPackageUpdate(); } else { var installer = AssetDatabase.LoadAssetAtPath("Assets/Plugins/PrimeTween/PrimeTweenInstaller.asset"); EditorUtility.FocusProjectWindow(); // this is important to show the installer object in the Project window Selection.activeObject = installer; EditorGUIUtility.PingObject(installer); EditorApplication.update += InstallAndUnsubscribeFromUpdate; void InstallAndUnsubscribeFromUpdate() { EditorApplication.update -= InstallAndUnsubscribeFromUpdate; installPlugin(); } } } }; } internal const string version = "1.3.1"; } internal static class FixedUpdateParameterMigration { internal static string[] FindLocalScriptGuids() { var listRequest = Client.List(true); while (!listRequest.IsCompleted) { } Assert.AreEqual(StatusCode.Success, listRequest.Status); string[] folders = listRequest.Result .Where(x => x.source == PackageSource.Embedded || x.source == PackageSource.Local) .Where(x => x.name != InstallerInspector.pluginPackageId) .Select(x => x.assetPath) .Append("Assets") .ToArray(); return AssetDatabase.FindAssets("t:Script", folders); } internal static bool Process(string[] scripts, bool? fixAutomatically = null) { var logSb = new StringBuilder(); var fileSb = new StringBuilder(); foreach (string guid in scripts) { string path = AssetDatabase.GUIDToAssetPath(guid); var textAsset = AssetDatabase.LoadAssetAtPath(path); string text = textAsset.text; if (!Regex.IsMatch(text, @"using PrimeTween\s*;")) { continue; } var parameterMatches = Regex.Matches(text, @"useFixedUpdate\s*:"); if (parameterMatches.Count > 0) { if (!fixAutomatically.HasValue) { return true; } logSb.Clear(); if (fixAutomatically.Value) { fileSb.Clear(); fileSb.Append(text); for (int i = parameterMatches.Count - 1; i >= 0; i--) { var paramMatch = parameterMatches[i]; fileSb.Remove(paramMatch.Index, paramMatch.Length); fileSb.Insert(paramMatch.Index, "updateType:"); } File.WriteAllText(path, fileSb.ToString()); logSb.Append($"PrimeTween automatically renamed ({parameterMatches.Count}) occurrences of 'useFixedUpdate' to 'updateType' in file '{textAsset.name}.cs':\n"); } else { logSb.Append($"PrimeTween: please rename ({parameterMatches.Count}) occurrences of 'useFixedUpdate' to 'updateType' in file '{textAsset.name}.cs':\n"); } var lineMatches = Regex.Matches(text, @"(?m)^.*useFixedUpdate\s*:.*$", RegexOptions.Multiline); foreach (Match match in lineMatches) { logSb.Append($"{match.Value.Trim().Replace("useFixedUpdate", "useFixedUpdate")}\n"); } Debug.unityLogger.Log(fixAutomatically.Value ? LogType.Warning : LogType.Error, logSb.ToString()); } } // no need to call AssetDatabase.Refresh() here because MoveAndRenameTgzArchive() already does that return false; } } internal static class ReviewRequest { const string version = InstallerInspector.version; const string canAskKey = "PrimeTween.canAskForReview"; const string versionKey = "PrimeTween.version"; internal static void OnPackageUpdate() { log("OnPackageUpdate"); if (!File.Exists(InstallerInspector.newTgzPath)) { Debug.LogError($"The installation archive is missing: '{InstallerInspector.newTgzPath}'. Please re-import PrimeTween from Asset Store."); return; } bool shouldAskForReview = true; var scriptGuids = FixedUpdateParameterMigration.FindLocalScriptGuids(); if (FixedUpdateParameterMigration.Process(scriptGuids)) { shouldAskForReview = false; const string msg = "'bool useFixedUpdate' parameter was changed to 'UpdateType updateType' in version 1.3.0, which will cause breaking changes in your current project.\n" + "PrimeTween can fix the breaking changes automatically, or you can fix them manually after the update.\n"; Debug.LogWarning($"PrimeTween: the {msg}"); int response = EditorUtility.DisplayDialogComplex($"{InstallerInspector.pluginName} {version}", $"The {msg}", "Fix automatically", "Cancel", "Fix manually"); string cancelMessage = $"PrimeTween: update to {version} was cancelled. You can trigger update manually from 'Assets/Plugins/PrimeTween/PrimeTweenInstaller'."; if (response == 1) { Debug.LogWarning(cancelMessage); return; } if (!EditorUtility.DisplayDialog($"{InstallerInspector.pluginName} {version}", "Please back up your project before proceeding.", "OK", "Cancel")) { Debug.LogWarning(cancelMessage); return; } bool fixAutomatically = response == 0; FixedUpdateParameterMigration.Process(scriptGuids, fixAutomatically); } InstallerInspector.MoveAndRenameTgzArchive(); if (UNITY_2018) { var removeRequest = Client.Remove(InstallerInspector.pluginPackageId); while (!removeRequest.IsCompleted) { } string path = $"file:../{InstallerInspector.tgzPath}"; Client.Add(path); } else { EditorApplication.ExecuteMenuItem("Assets/Refresh"); // AssetDatabase.Refresh() refreshes the project only partially } string prevVersion = savedVersion; if (savedVersion == version) { log($"same version {version}"); return; } savedVersion = version; if (InsertCallbackBug.IsUpdatingFromVersionWithBug(prevVersion)) { InsertCallbackBug.Find(); EditorPrefs.SetBool(InsertCallbackBug.showInsertCallbackBugUi, true); shouldAskForReview = false; } else { EditorPrefs.SetBool(InsertCallbackBug.showInsertCallbackBugUi, false); } log($"updated from version {prevVersion} to {version}, {nameof(shouldAskForReview)}: {shouldAskForReview}"); if (shouldAskForReview) { TryAskForReview(); } } static bool UNITY_2018 { get { #if UNITY_2018 return true; #else return false; #endif } } static void TryAskForReview() { if (!EditorPrefs.GetBool(canAskKey, true)) { log("can't ask"); return; } DisableReviewRequest(); var response = EditorUtility.DisplayDialogComplex("Enjoying PrimeTween?", "Would you mind to leave an honest review on Asset store? Honest reviews make PrimeTween better and help other developers discover it.", "Sure, leave a review!", "Never ask again", ""); if (response == 0) { OpenReviewsURL(); } } internal static void OnBeforeInstall() { log($"OnBeforeInstall {version}"); if (string.IsNullOrEmpty(savedVersion)) { savedVersion = version; } } static string savedVersion { get => EditorPrefs.GetString(versionKey); set => EditorPrefs.SetString(versionKey, value); } internal static void DisableReviewRequest() => EditorPrefs.SetBool(canAskKey, false); internal static void OpenReviewsURL() => Application.OpenURL("https://assetstore.unity.com/packages/slug/252960#reviews"); internal static void ResetReviewRequest() { Debug.Log(nameof(ResetReviewRequest)); EditorPrefs.DeleteKey(versionKey); EditorPrefs.DeleteKey(canAskKey); } internal static void DebugReviewRequest() { Debug.Log(nameof(DebugReviewRequest)); savedVersion = "1.1.22"; EditorPrefs.SetBool(canAskKey, false); // TryAskForReview(); } [System.Diagnostics.Conditional("_")] static void log(string msg) { Debug.Log($"ReviewRequest: {msg}"); } } internal static class InsertCallbackBug { internal const string moreInfoUrl = "https://github.com/KyryloKuzyk/PrimeTween/discussions/112"; internal const string showInsertCallbackBugUi = "PrimeTween.showInsertCallbackBugUi"; static Dictionary OpCodeDict; static MethodInfo[] methodsWithBug; static MethodInfo[] groupMethods; internal static bool IsUpdatingFromVersionWithBug(string prevVersionString) { if (Version.TryParse(prevVersionString, out var prevVersion) && new Version(1, 1, 10) <= prevVersion && prevVersion <= new Version(1, 1, 22) ) { return true; } return false; } internal static void Find() { OpCodeDict = typeof(OpCodes) .GetFields(BindingFlags.Public | BindingFlags.Static) .Select(x => (OpCode)x.GetValue(null)) .ToDictionary(x => x.Value, x => x); #if PRIME_TWEEN_INSTALLED methodsWithBug = typeof(Sequence).GetMethods(BindingFlags.Public | BindingFlags.Instance) .Where(methodInfo => methodInfo.Name == nameof(Sequence.ChainCallback) || methodInfo.Name == nameof(Sequence.InsertCallback)) .Select(methodInfo => methodInfo.IsGenericMethod ? methodInfo.GetGenericMethodDefinition() : methodInfo) .ToArray(); Assert.AreEqual(4, methodsWithBug.Length); groupMethods = typeof(Sequence).GetMethods(BindingFlags.Public | BindingFlags.Instance) .Where(methodInfo => methodInfo.Name == nameof(Sequence.Group)) .ToArray(); #endif Assert.AreEqual(2, groupMethods.Length); string methodAssemblyName = methodsWithBug[0].Module.Assembly.FullName; const BindingFlags findAll = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; int numPotentialIssues = AppDomain.CurrentDomain.GetAssemblies() .Where(assembly => assembly.GetReferencedAssemblies().Any(dependency => dependency.FullName == methodAssemblyName)) .Where(assembly => !assembly.GetName().Name.StartsWith("PrimeTween.", StringComparison.Ordinal)) .SelectMany(assembly => assembly.GetTypes()) .SelectMany(type => type.GetMethods(findAll).Cast().Union(type.GetConstructors(findAll))) .Count(method => FindInMethod(method)); if (numPotentialIssues == 0) { Debug.Log($"PrimeTween updated to version {InstallerInspector.version}: no potential issues found in ChainCallback() and InsertCallback() usages.\n" + $"More info: {moreInfoUrl}\n"); } int updateResponse = EditorUtility.DisplayDialogComplex($"{InstallerInspector.pluginName} {InstallerInspector.version}", "PrimeTween 1.2.0 fixed a bug in ChainCallback() and InsertCallback() methods.\n" + "This fix may introduce breaking changes in the existing projects. Please see the Console output for more details.", "More info", "Close", ""); if (updateResponse == 0) { Application.OpenURL(moreInfoUrl); } } /// https://stackoverflow.com/a/33034906/1951038 static bool FindInMethod(MethodBase method) { byte[] il = method.GetMethodBody()?.GetILAsByteArray(); if (il == null) { return false; } bool bugFound = false; using (var br = new BinaryReader(new MemoryStream(il))) { while (br.BaseStream.Position < br.BaseStream.Length) { byte firstByte = br.ReadByte(); short opCodeValue = firstByte == 0xFE ? BitConverter.ToInt16(new[] { br.ReadByte(), firstByte }, 0) : firstByte; OpCode opCode = OpCodeDict[opCodeValue]; switch (opCode.OperandType) { case OperandType.ShortInlineBrTarget: case OperandType.ShortInlineVar: case OperandType.ShortInlineI: br.ReadByte(); break; case OperandType.InlineVar: br.ReadInt16(); break; case OperandType.InlineField: case OperandType.InlineType: case OperandType.ShortInlineR: case OperandType.InlineString: case OperandType.InlineSig: case OperandType.InlineI: case OperandType.InlineBrTarget: br.ReadInt32(); break; case OperandType.InlineI8: case OperandType.InlineR: br.ReadInt64(); break; case OperandType.InlineSwitch: var size = (int)br.ReadUInt32(); br.ReadBytes(size * 4); break; case OperandType.InlineTok: br.ReadUInt32(); break; case OperandType.InlineMethod: int token = (int)br.ReadUInt32(); if (method.Module.ResolveMethod(token) is MethodInfo resolvedMethod) { if (bugFound) { if (groupMethods.Contains(resolvedMethod)) { Debug.LogError($"PrimeTween updated to version {InstallerInspector.version}: potential breaking change found in the '{method.DeclaringType}.{method.Name}()' method.\n" + "Please double-check the behavior if Group() is called immediately after the ChainCallback() or InsertCallback() and apply the fix manually if necessary.\n" + "Or use ChainCallbackObsolete/InsertCallbackObsolete() instead to preserve the old incorrect behavior.\n" + $"More info: {moreInfoUrl}\n"); return true; } } else { bugFound = isMethodWithBug(resolvedMethod); } } break; case OperandType.InlineNone: break; default: throw new Exception(); } } } return false; } static bool isMethodWithBug(MethodInfo method) { foreach (var methodWithBug in methodsWithBug) { if (methodWithBug.IsGenericMethodDefinition && method.IsGenericMethod) { if (methodWithBug == method.GetGenericMethodDefinition()) { return true; } } else if (methodWithBug == method) { return true; } } return false; } } }