From ad54d2c76b5cd09f5192684a2823a7d9417091e8 Mon Sep 17 00:00:00 2001 From: sinaioutlander <49360850+sinaioutlander@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:19:56 +1100 Subject: [PATCH] 2.0.2 * Added support for viewing Texture2D (and Sprite) from the Inspector, and exporting them to PNG * Fixed an issue with generic methods not showing their return value type * Fixed an issue where destroyed UnityEngine.Objects would cause issues in the inspector * Fixed an issue when caching a ValueCollection of a Dictionary (the generic argument for the Entry Type is the last arg, not the first as with other Enumerables) --- src/CacheObject/CacheMember.cs | 2 +- src/CacheObject/CacheMethod.cs | 2 +- src/CacheObject/CacheObjectBase.cs | 10 + src/Config/ModConfig.cs | 2 +- src/Explorer.csproj | 3 + src/Tests/TestClass.cs | 14 + src/UI/Inspectors/ReflectionInspector.cs | 2 +- src/UI/InteractiveValue/InteractiveValue.cs | 2 +- .../Object/InteractiveEnumerable.cs | 47 +--- .../Object/InteractiveSprite.cs | 28 ++ .../Object/InteractiveTexture2D.cs | 254 ++++++++++++++++++ src/UI/Main/OptionsPage.cs | 10 + .../ImageConversion/ImageConversionUnstrip.cs | 51 ++++ src/Unstrip/Scene/SceneUnstrip.cs | 30 +-- 14 files changed, 400 insertions(+), 57 deletions(-) create mode 100644 src/UI/InteractiveValue/Object/InteractiveSprite.cs create mode 100644 src/UI/InteractiveValue/Object/InteractiveTexture2D.cs create mode 100644 src/Unstrip/ImageConversion/ImageConversionUnstrip.cs diff --git a/src/CacheObject/CacheMember.cs b/src/CacheObject/CacheMember.cs index 5cae830..cf35a57 100644 --- a/src/CacheObject/CacheMember.cs +++ b/src/CacheObject/CacheMember.cs @@ -129,7 +129,7 @@ namespace Explorer.CacheObject GUILayout.Label(i.ToString(), new GUILayoutOption[] { GUILayout.Width(15) }); GUILayout.Label(label, new GUILayoutOption[] { GUILayout.ExpandWidth(false) }); - this.m_argumentInput[i] = GUILayout.TextField(input, new GUILayoutOption[] { GUILayout.ExpandWidth(true) }); + this.m_argumentInput[i] = GUIUnstrip.TextField(input, new GUILayoutOption[] { GUILayout.ExpandWidth(true) }); GUI.skin.label.alignment = TextAnchor.MiddleLeft; diff --git a/src/CacheObject/CacheMethod.cs b/src/CacheObject/CacheMethod.cs index 1b3e012..b989321 100644 --- a/src/CacheObject/CacheMethod.cs +++ b/src/CacheObject/CacheMethod.cs @@ -73,7 +73,7 @@ namespace Explorer.CacheObject if (ret != null) { //m_cachedReturnValue = CacheFactory.GetTypeAndCacheObject(ret); - m_cachedReturnValue = CacheFactory.GetCacheObject(ret, IValue.ValueType); + m_cachedReturnValue = CacheFactory.GetCacheObject(ret); m_cachedReturnValue.UpdateValue(); } else diff --git a/src/CacheObject/CacheObjectBase.cs b/src/CacheObject/CacheObjectBase.cs index 270529d..16e69b2 100644 --- a/src/CacheObject/CacheObjectBase.cs +++ b/src/CacheObject/CacheObjectBase.cs @@ -25,12 +25,22 @@ namespace Explorer.CacheObject return; } + //ExplorerCore.Log("Initializing InteractiveValue of type " + valueType.FullName); + InteractiveValue interactive; if (valueType == typeof(GameObject) || valueType == typeof(Transform)) { interactive = new InteractiveGameObject(); } + else if (valueType == typeof(Texture2D)) + { + interactive = new InteractiveTexture2D(); + } + else if (valueType == typeof(Sprite)) + { + interactive = new InteractiveSprite(); + } else if (valueType.IsPrimitive || valueType == typeof(string)) { interactive = new InteractivePrimitive(); diff --git a/src/Config/ModConfig.cs b/src/Config/ModConfig.cs index b7ffd2e..8c016f4 100644 --- a/src/Config/ModConfig.cs +++ b/src/Config/ModConfig.cs @@ -19,7 +19,7 @@ namespace Explorer.Config public int Default_Page_Limit = 20; public bool Bitwise_Support = false; public bool Tab_View = true; - //public bool Main_Toggle_Global = true; + public string Default_Output_Path = @"Mods\Explorer"; public static void OnLoad() { diff --git a/src/Explorer.csproj b/src/Explorer.csproj index 0469294..3200986 100644 --- a/src/Explorer.csproj +++ b/src/Explorer.csproj @@ -212,6 +212,8 @@ + + @@ -257,6 +259,7 @@ + diff --git a/src/Tests/TestClass.cs b/src/Tests/TestClass.cs index a220d44..5ec70f3 100644 --- a/src/Tests/TestClass.cs +++ b/src/Tests/TestClass.cs @@ -4,6 +4,7 @@ using System; using UnityEngine; using System.Reflection; using System.Runtime.InteropServices; +using Explorer.UI.Shared; #if CPP using UnhollowerBaseLib; using UnityEngine.SceneManagement; @@ -33,6 +34,9 @@ namespace Explorer.Tests public static TestClass Instance => m_instance ?? (m_instance = new TestClass()); private static TestClass m_instance; + public Texture2D TestTexture = UIStyles.MakeTex(200, 200, Color.white); + public static Sprite TestSprite; + public static int StaticProperty => 5; public static int StaticField = 5; public int NonStaticField; @@ -52,6 +56,16 @@ namespace Explorer.Tests public TestClass() { #if CPP + TestTexture.name = "TestTexture"; + + var r = new Rect(0, 0, TestTexture.width, TestTexture.height); + var v2 = Vector2.zero; + var v4 = Vector4.zero; + TestSprite = Sprite.CreateSprite_Injected(TestTexture, ref r, ref v2, 100f, 0u, SpriteMeshType.Tight, ref v4, false); + + GameObject.DontDestroyOnLoad(TestTexture); + GameObject.DontDestroyOnLoad(TestSprite); + ILHashSetTest = new Il2CppSystem.Collections.Generic.HashSet(); ILHashSetTest.Add("1"); ILHashSetTest.Add("2"); diff --git a/src/UI/Inspectors/ReflectionInspector.cs b/src/UI/Inspectors/ReflectionInspector.cs index 2fcbfc7..cbc802c 100644 --- a/src/UI/Inspectors/ReflectionInspector.cs +++ b/src/UI/Inspectors/ReflectionInspector.cs @@ -160,7 +160,7 @@ namespace Explorer.UI.Inspectors { try { - // make sure member type is Field, Method of Property (4 / 8 / 16) + // make sure member type is Field, Method or Property (4 / 8 / 16) int m = (int)member.MemberType; if (m < 4 || m > 16) continue; diff --git a/src/UI/InteractiveValue/InteractiveValue.cs b/src/UI/InteractiveValue/InteractiveValue.cs index ad9e2be..5b2ffad 100644 --- a/src/UI/InteractiveValue/InteractiveValue.cs +++ b/src/UI/InteractiveValue/InteractiveValue.cs @@ -146,7 +146,7 @@ namespace Explorer.UI { GUILayout.Label($"Not yet evaluated ({typeName})", new GUILayoutOption[0]); } - else if (Value == null && !(cacheMember is CacheMethod)) + else if ((Value == null || Value is UnityEngine.Object uObj && !uObj) && !(cacheMember is CacheMethod)) { GUILayout.Label($"null ({typeName})", new GUILayoutOption[0]); } diff --git a/src/UI/InteractiveValue/Object/InteractiveEnumerable.cs b/src/UI/InteractiveValue/Object/InteractiveEnumerable.cs index e863390..8d62f00 100644 --- a/src/UI/InteractiveValue/Object/InteractiveEnumerable.cs +++ b/src/UI/InteractiveValue/Object/InteractiveEnumerable.cs @@ -173,38 +173,20 @@ namespace Explorer.UI private Type GetEntryType() { - if (m_entryType == null) + if (ValueType.IsGenericType) { - if (OwnerCacheObject is CacheMember cacheMember && cacheMember.MemInfo != null) - { - Type memberType = null; - switch (cacheMember.MemInfo.MemberType) - { - case MemberTypes.Field: - memberType = (cacheMember.MemInfo as FieldInfo).FieldType; - break; - case MemberTypes.Property: - memberType = (cacheMember.MemInfo as PropertyInfo).PropertyType; - break; - } + var gArgs = ValueType.GetGenericArguments(); - if (memberType != null && memberType.IsGenericType) - { - m_entryType = memberType.GetGenericArguments()[0]; - } - } - else if (Value != null) + if (ValueType.FullName.Contains("ValueCollection")) { - var type = Value.GetType(); - if (type.IsGenericType) - { - m_entryType = type.GetGenericArguments()[0]; - } + m_entryType = gArgs[gArgs.Length - 1]; + } + else + { + m_entryType = gArgs[0]; } } - - // use System.Object for non-generic. - if (m_entryType == null) + else { m_entryType = typeof(object); } @@ -255,18 +237,11 @@ namespace Explorer.UI } #endif + //ExplorerCore.Log("Caching enumeration entry " + obj.ToString() + " as " + EntryType.FullName); + var cached = new CacheEnumerated() { Index = index, RefIList = Value as IList, ParentEnumeration = this }; cached.Init(obj, EntryType); list.Add(cached); - - //if (CacheFactory.GetCacheObject(obj, t) is CacheObjectBase cached) - //{ - // list.Add(cached); - //} - //else - //{ - // list.Add(null); - //} } else { diff --git a/src/UI/InteractiveValue/Object/InteractiveSprite.cs b/src/UI/InteractiveValue/Object/InteractiveSprite.cs new file mode 100644 index 0000000..cfed55b --- /dev/null +++ b/src/UI/InteractiveValue/Object/InteractiveSprite.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace Explorer.UI +{ + public class InteractiveSprite : InteractiveTexture2D + { + public override void GetTexture2D() + { +#if CPP + if (Value != null && Value.Il2CppCast(typeof(Sprite)) is Sprite sprite) +#else + if (Value is Sprite sprite) +#endif + { + currentTex = sprite.texture; + texContent = new GUIContent + { + image = currentTex + }; + } + } + + } +} diff --git a/src/UI/InteractiveValue/Object/InteractiveTexture2D.cs b/src/UI/InteractiveValue/Object/InteractiveTexture2D.cs new file mode 100644 index 0000000..ed2cf38 --- /dev/null +++ b/src/UI/InteractiveValue/Object/InteractiveTexture2D.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Explorer.CacheObject; +using Explorer.Config; +using UnityEngine; +using System.IO; +#if CPP +using Explorer.Unstrip.ImageConversion; +#endif + +namespace Explorer.UI +{ + public class InteractiveTexture2D : InteractiveValue, IExpandHeight + { + public bool IsExpanded { get; set; } + public float WhiteSpace { get; set; } = 215f; + + public Texture2D currentTex; + public GUIContent texContent; + + private string saveFolder = ModConfig.Instance.Default_Output_Path; + + public override void Init() + { + base.Init(); + } + + public override void UpdateValue() + { + base.UpdateValue(); + + GetTexture2D(); + } + + public virtual void GetTexture2D() + { +#if CPP + if (Value != null && Value.Il2CppCast(typeof(Texture2D)) is Texture2D tex) +#else + if (Value is Texture2D tex) +#endif + { + currentTex = tex; + texContent = new GUIContent + { + image = currentTex + }; + } + } + + public override void DrawValue(Rect window, float width) + { + GUIUnstrip.BeginVertical(); + + GUIUnstrip.BeginHorizontal(); + + if (currentTex) + { + if (!IsExpanded) + { + if (GUILayout.Button("v", new GUILayoutOption[] { GUILayout.Width(25) })) + { + IsExpanded = true; + } + } + else + { + if (GUILayout.Button("^", new GUILayoutOption[] { GUILayout.Width(25) })) + { + IsExpanded = false; + } + } + } + + base.DrawValue(window, width); + + GUILayout.EndHorizontal(); + + if (currentTex && IsExpanded) + { + DrawTextureControls(); + DrawTexture(); + } + + GUILayout.EndVertical(); + } + + // Temporarily disabled in BepInEx IL2CPP. + private void DrawTexture() + { +#if CPP +#if BIE +#else + GUILayout.Label(texContent, new GUILayoutOption[0]); +#endif +#else + GUILayout.Label(texContent, new GUILayoutOption[0]); +#endif + } + + private void DrawTextureControls() + { + GUIUnstrip.BeginHorizontal(); + + GUILayout.Label("Save folder:", new GUILayoutOption[] { GUILayout.Width(80f) }); + saveFolder = GUIUnstrip.TextField(saveFolder, new GUILayoutOption[0]); + GUIUnstrip.Space(10f); + + GUILayout.EndHorizontal(); + + if (GUILayout.Button("Save to PNG", new GUILayoutOption[] { GUILayout.Width(100f) })) + { + if (currentTex) + { + var name = RemoveInvalidFilenameChars(currentTex.name ?? ""); + if (string.IsNullOrEmpty(name)) + { + if (OwnerCacheObject is CacheMember cacheMember) + { + name = cacheMember.MemInfo.Name; + } + else + { + name = "UNTITLED"; + } + } + + SaveTextureAsPNG(currentTex, saveFolder, name, false); + + ExplorerCore.Log($@"Saved to {saveFolder}\{name}.png!"); + } + else + { + ExplorerCore.Log("Cannot save a null texture!"); + } + } + } + + private string RemoveInvalidFilenameChars(string s) + { + var invalid = System.IO.Path.GetInvalidFileNameChars(); + foreach (var c in invalid) + { + s = s.Replace(c.ToString(), ""); + } + return s; + } + + public static void SaveTextureAsPNG(Texture2D tex, string dir, string name, bool isDTXnmNormal = false) + { + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + byte[] data; + var savepath = dir + @"\" + name + ".png"; + + try + { + if (isDTXnmNormal) + { + tex = DTXnmToRGBA(tex); + tex.Apply(false, false); + } + + data = tex.EncodeToPNG(); + + if (data == null) + { + ExplorerCore.Log("Couldn't get data with EncodeToPNG (probably ReadOnly?), trying manually..."); + throw new Exception(); + } + } + catch + { + var origFilter = tex.filterMode; + tex.filterMode = FilterMode.Point; + + RenderTexture rt = RenderTexture.GetTemporary(tex.width, tex.height); + rt.filterMode = FilterMode.Point; + RenderTexture.active = rt; + Graphics.Blit(tex, rt); + + Texture2D _newTex = new Texture2D(tex.width, tex.height, TextureFormat.RGBA32, false); + _newTex.ReadPixels(new Rect(0, 0, tex.width, tex.height), 0, 0); + + if (isDTXnmNormal) + { + _newTex = DTXnmToRGBA(_newTex); + } + + _newTex.Apply(false, false); + + RenderTexture.active = null; + tex.filterMode = origFilter; + + data = _newTex.EncodeToPNG(); + //data = _newTex.GetRawTextureData(); + } + + if (data == null || data.Length < 1) + { + ExplorerCore.LogWarning("Couldn't get any data for the texture!"); + } + else + { +#if CPP + // The IL2CPP method will return invalid byte data. + // However, we can just iterate into safe C# byte[] array. + byte[] safeData = new byte[data.Length]; + for (int i = 0; i < data.Length; i++) + { + safeData[i] = (byte)data[i]; // not sure if cast is needed + } + + File.WriteAllBytes(savepath, safeData); +#else + File.WriteAllBytes(savepath, data); +#endif + } + } + + // Converts DTXnm-format Normal Map to RGBA-format Normal Map. + public static Texture2D DTXnmToRGBA(Texture2D tex) + { + Color[] colors = tex.GetPixels(); + + for (int i = 0; i < colors.Length; i++) + { + Color c = colors[i]; + + c.r = c.a * 2 - 1; // red <- alpha + c.g = c.g * 2 - 1; // green is always the same + + Vector2 rg = new Vector2(c.r, c.g); //this is the red-green vector + c.b = Mathf.Sqrt(1 - Mathf.Clamp01(Vector2.Dot(rg, rg))); //recalculate the blue channel + + colors[i] = new Color( + (c.r * 0.5f) + 0.5f, + (c.g * 0.5f) + 0.25f, + (c.b * 0.5f) + 0.5f + ); + } + + var newtex = new Texture2D(tex.width, tex.height, TextureFormat.RGBA32, false); + newtex.SetPixels(colors); + + return newtex; + } + } +} diff --git a/src/UI/Main/OptionsPage.cs b/src/UI/Main/OptionsPage.cs index e3e28d2..42e071a 100644 --- a/src/UI/Main/OptionsPage.cs +++ b/src/UI/Main/OptionsPage.cs @@ -18,12 +18,14 @@ namespace Explorer.UI.Main public int defaultPageLimit; public bool bitwiseSupport; public bool tabView; + public string defaultOutputPath; private CacheObjectBase toggleKeyInput; private CacheObjectBase defaultSizeInput; private CacheObjectBase defaultPageLimitInput; private CacheObjectBase bitwiseSupportInput; private CacheObjectBase tabViewInput; + private CacheObjectBase defaultOutputPathInput; public override void Init() { @@ -41,6 +43,9 @@ namespace Explorer.UI.Main tabView = ModConfig.Instance.Tab_View; tabViewInput = CacheFactory.GetCacheObject(typeof(OptionsPage).GetField("tabView"), this); + + defaultOutputPath = ModConfig.Instance.Default_Output_Path; + defaultOutputPathInput = CacheFactory.GetCacheObject(typeof(OptionsPage).GetField("defaultOutputPath"), this); } public override void Update() { } @@ -78,6 +83,11 @@ namespace Explorer.UI.Main tabViewInput.IValue.DrawValue(MainMenu.MainRect, MainMenu.MainRect.width - 215f); GUILayout.EndHorizontal(); + GUIUnstrip.BeginHorizontal(new GUILayoutOption[0]); + GUILayout.Label($"Default Output Path:", new GUILayoutOption[] { GUILayout.Width(215f) }); + defaultOutputPathInput.IValue.DrawValue(MainMenu.MainRect, MainMenu.MainRect.width - 215f); + GUILayout.EndHorizontal(); + if (GUILayout.Button("Apply and Save", new GUILayoutOption[0])) { ApplyAndSave(); diff --git a/src/Unstrip/ImageConversion/ImageConversionUnstrip.cs b/src/Unstrip/ImageConversion/ImageConversionUnstrip.cs new file mode 100644 index 0000000..97d9986 --- /dev/null +++ b/src/Unstrip/ImageConversion/ImageConversionUnstrip.cs @@ -0,0 +1,51 @@ +#if CPP +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnhollowerBaseLib; +using UnityEngine; +using System.IO; + +namespace Explorer.Unstrip.ImageConversion +{ + public static class ImageConversionUnstrip + { + // byte[] ImageConversion.EncodeToPNG(this Texture2D image); + + public static byte[] EncodeToPNG(this Texture2D tex) + { + return EncodeToPNG_iCall(tex.Pointer); + } + + internal delegate byte[] EncodeToPNG_delegate(IntPtr tex); + internal static EncodeToPNG_delegate EncodeToPNG_iCall = + IL2CPP.ResolveICall("UnityEngine.ImageConversion::EncodeToPNG"); + + // bool ImageConversion.LoadImage(this Texture2D tex, byte[] data, bool markNonReadable); + + public static bool LoadImage(this Texture2D tex, byte[] data, bool markNonReadable) + { + return LoadImage_iCall(tex.Pointer, data, markNonReadable); + } + + internal delegate bool LoadImage_delegate(IntPtr tex, byte[] data, bool markNonReadable); + internal static LoadImage_delegate LoadImage_iCall = + IL2CPP.ResolveICall("UnityEngine.ImageConversion::LoadImage"); + + // Helper for LoadImage + + public static bool LoadImage(this Texture2D tex, string filePath, bool markNonReadable) + { + if (!File.Exists(filePath)) + { + return false; + } + + var data = File.ReadAllBytes(filePath); + return tex.LoadImage(data, markNonReadable); + } + } +} + +#endif \ No newline at end of file diff --git a/src/Unstrip/Scene/SceneUnstrip.cs b/src/Unstrip/Scene/SceneUnstrip.cs index deaab91..25fb324 100644 --- a/src/Unstrip/Scene/SceneUnstrip.cs +++ b/src/Unstrip/Scene/SceneUnstrip.cs @@ -5,37 +5,35 @@ using System.Linq; using System.Text; using UnhollowerBaseLib; using UnityEngine; +using UnityEngine.SceneManagement; -namespace Explorer.Unstrip.Scene +namespace Explorer.Unstrip.Scenes { public class SceneUnstrip { - internal delegate void getRootSceneObjects(int handle, IntPtr list); - internal static getRootSceneObjects getRootSceneObjects_iCall = - IL2CPP.ResolveICall("UnityEngine.SceneManagement.Scene::GetRootGameObjectsInternal"); - - public static void GetRootGameObjects_Internal(UnityEngine.SceneManagement.Scene scene, IntPtr list) - { - getRootSceneObjects_iCall(scene.handle, list); - } - - public static GameObject[] GetRootSceneObjects(UnityEngine.SceneManagement.Scene scene) + //Scene.GetRootGameObjects(); + public static GameObject[] GetRootGameObjects(Scene scene) { var list = new Il2CppSystem.Collections.Generic.List(GetRootCount_Internal(scene)); - GetRootGameObjects_Internal(scene, list.Pointer); + GetRootGameObjectsInternal_iCall(scene.handle, list.Pointer); return list.ToArray(); } - internal delegate int getRootCount(int handle); - internal static getRootCount getRootCount_iCall = - IL2CPP.ResolveICall("UnityEngine.SceneManagement.Scene::GetRootCountInternal"); + internal delegate void GetRootGameObjectsInternal_delegate(int handle, IntPtr list); + internal static GetRootGameObjectsInternal_delegate GetRootGameObjectsInternal_iCall = + IL2CPP.ResolveICall("UnityEngine.SceneManagement.Scene::GetRootGameObjectsInternal"); + //Scene.rootCount; public static int GetRootCount_Internal(UnityEngine.SceneManagement.Scene scene) { - return getRootCount_iCall(scene.handle); + return GetRootCountInternal_iCall(scene.handle); } + + internal delegate int GetRootCountInternal_delegate(int handle); + internal static GetRootCountInternal_delegate GetRootCountInternal_iCall = + IL2CPP.ResolveICall("UnityEngine.SceneManagement.Scene::GetRootCountInternal"); } } #endif \ No newline at end of file