Compare commits

..

33 Commits

Author SHA1 Message Date
7a5570a070 Bump version 2022-04-09 18:59:34 +10:00
d40537775f Bump UniverseLib 2022-04-09 18:59:13 +10:00
9b6f3fd3ea Cleanups / refactoring 2022-04-09 18:58:56 +10:00
bacac929e9 Show InputField for exceptions to view/copy full exception 2022-04-01 18:14:50 +11:00
4e3d3a2e5c Update README.md 2022-04-01 01:37:51 +11:00
7dbc8fd66e Merge branch 'master' of https://github.com/sinai-dev/UnityExplorer 2022-04-01 01:31:53 +11:00
892cefcc91 Bump version 2022-04-01 01:31:45 +11:00
a986b92963 Don't try to get UnityObjectWidget for static class inspection 2022-04-01 01:31:40 +11:00
8837119781 Merge pull request #132 from liesauer/master
Fix standalone reading config item type casting bug
2022-04-01 01:21:39 +11:00
7eda249ddb Fix standalone reading config item type casting bug 2022-03-31 21:07:06 +08:00
710b4ba74a Use switch expression instead of if/else block 2022-03-29 22:39:26 +11:00
4bee55fb25 Cleanups, remove redundancy 2022-03-29 22:36:17 +11:00
c71748d22a Remove pointless ToArray() call 2022-03-23 19:02:17 +11:00
621035c732 Bump UniverseLib 2022-03-23 18:04:27 +11:00
2285a495be Add support for Unity Editor release 2022-03-21 21:25:51 +11:00
efdf2446bd A few cleanups and optimizations 2022-03-21 21:25:18 +11:00
6ad45ac8ae Delete social.png 2022-03-21 21:24:36 +11:00
2d3c83cfa3 Bump version 2022-03-21 01:05:00 +11:00
3213717ff6 Add outline to resize arrow 2022-03-21 01:04:55 +11:00
a6a1a4d046 cleanup 2022-03-21 01:04:44 +11:00
078c2e2b51 Add AudioClipWidget 2022-03-21 01:04:39 +11:00
49bce650b4 Fix text clipping with transform cell name and sibling index 2022-03-20 21:21:23 +11:00
bd9e80f2b4 Refactor unity inspector widgets into proper classes 2022-03-20 21:21:01 +11:00
d1eb5671bf Move GetCacheMembers into separate class 2022-03-20 21:20:17 +11:00
f1ca484712 Fix generic arguments for parameterless struct ctors 2022-03-16 23:21:39 +11:00
3b71b40843 More accurate check if enum value has flag 2022-03-15 01:51:27 +11:00
1292affe6d Fix autocomplete panel resizing 2022-03-15 01:50:50 +11:00
6614762fe8 Bump version 2022-03-15 01:19:52 +11:00
d81d6d034b Implement support for implicit struct constructors 2022-03-15 01:19:48 +11:00
5dfe3bbf0c Fix missing and incorrect ctor caching
- Shouldn't include ctors on abstract types
- Missing parameterless ctors for structs (implicit)
2022-03-15 01:19:01 +11:00
dc81451ce5 Make Constructor toggle button actually do something 2022-03-15 01:18:12 +11:00
d7ab0a23c6 Skip private/anonymous types in TypeCompleter results 2022-03-15 01:17:54 +11:00
1a01c740e2 Move AutoCompleteModal into Panels folder 2022-03-15 01:17:35 +11:00
35 changed files with 1402 additions and 700 deletions

View File

@ -45,12 +45,18 @@
The standalone release can be used with any injector or loader of your choice, but it requires you to load the dependencies manually.
1. Ensure the required libs are loaded - UniverseLib, HarmonyX and MonoMod
1. Ensure the required libs are loaded - UniverseLib, HarmonyX and MonoMod. Take them from the [`UnityExplorer.Editor`](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.Editor.zip) release if you need them.
2. For IL2CPP, load Il2CppAssemblyUnhollower and start an [Il2CppAssemblyUnhollower runtime](https://github.com/knah/Il2CppAssemblyUnhollower#required-external-setup)
2. Load the UnityExplorer DLL
3. Create an instance of Unity Explorer with `UnityExplorer.ExplorerStandalone.CreateInstance();`
4. Optionally subscribe to the `ExplorerStandalone.OnLog` event to handle logging if you wish
## Unity Editor
1. Download the [`UnityExplorer.Editor`](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.Editor.zip) release.
2. Install the package, either by using the Package Manager and importing the `package.json` file, or by manually dragging the folder into your `Assets` folder.
3. Drag the `Runtime/UnityExplorer` prefab into your scene, or create a GameObject and add the `Explorer Editor Behaviour` script to it.
# Common issues and solutions
Although UnityExplorer should work out of the box for most Unity games, in some cases you may need to tweak the settings for it to work properly.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

View File

@ -11,8 +11,9 @@ namespace UnityExplorer.CacheObject
public class CacheConstructor : CacheMember
{
public ConstructorInfo CtorInfo { get; }
readonly Type typeForStructConstructor;
public override Type DeclaringType => CtorInfo.DeclaringType;
public override Type DeclaringType => typeForStructConstructor ?? CtorInfo.DeclaringType;
public override bool IsStatic => true;
public override bool ShouldAutoEvaluate => false;
public override bool CanWrite => false;
@ -22,13 +23,38 @@ namespace UnityExplorer.CacheObject
this.CtorInfo = ci;
}
public CacheConstructor(Type typeForStructConstructor)
{
this.typeForStructConstructor = typeForStructConstructor;
}
public override void SetInspectorOwner(ReflectionInspector inspector, MemberInfo member)
{
base.SetInspectorOwner(inspector, member);
Type ctorReturnType;
// if is parameterless struct ctor
if (typeForStructConstructor != null)
{
ctorReturnType = typeForStructConstructor;
this.Owner = inspector;
Arguments = CtorInfo.GetParameters();
if (CtorInfo.DeclaringType.IsGenericTypeDefinition)
GenericArguments = CtorInfo.DeclaringType.GetGenericArguments();
// eg. Vector3.Vector3()
this.NameLabelText = SignatureHighlighter.Parse(typeForStructConstructor, false);
NameLabelText += $".{NameLabelText}()";
this.NameForFiltering = SignatureHighlighter.RemoveHighlighting(NameLabelText);
this.NameLabelTextRaw = NameForFiltering;
return;
}
else
{
base.SetInspectorOwner(inspector, member);
Arguments = CtorInfo.GetParameters();
ctorReturnType = CtorInfo.DeclaringType;
}
if (ctorReturnType.IsGenericTypeDefinition)
GenericArguments = ctorReturnType.GetGenericArguments();
}
protected override object TryEvaluate()
@ -46,13 +72,11 @@ namespace UnityExplorer.CacheObject
else
ret = Activator.CreateInstance(returnType, ArgumentUtility.EmptyArgs);
HadException = false;
LastException = null;
return ret;
}
catch (Exception ex)
{
HadException = true;
LastException = ex;
return null;
}

View File

@ -32,13 +32,11 @@ namespace UnityExplorer.CacheObject
try
{
var ret = FieldInfo.GetValue(DeclaringInstance);
HadException = false;
LastException = null;
return ret;
}
catch (Exception ex)
{
HadException = true;
LastException = ex;
return null;
}

View File

@ -169,174 +169,5 @@ namespace UnityExplorer.CacheObject
}
}
}
#region Cache Member Util
public static List<CacheMember> GetCacheMembers(object inspectorTarget, Type type, ReflectionInspector inspector)
{
//var list = new List<CacheMember>();
HashSet<string> cachedSigs = new();
List<CacheMember> props = new();
List<CacheMember> fields = new();
List<CacheMember> ctors = new();
List<CacheMember> methods = new();
var types = ReflectionUtility.GetAllBaseTypes(type);
var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static;
if (!inspector.StaticOnly)
flags |= BindingFlags.Instance;
// Get non-static constructors of the main type.
// There's no reason to get the static cctor, it will be invoked when we inspect the class.
// Also no point getting ctors on inherited types.
foreach (var ctor in type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
TryCacheMember(ctor, ctors, cachedSigs, type, inspector);
foreach (var declaringType in types)
{
var target = inspectorTarget;
if (!inspector.StaticOnly)
target = target.TryCast(declaringType);
foreach (var prop in declaringType.GetProperties(flags))
if (prop.DeclaringType == declaringType)
TryCacheMember(prop, props, cachedSigs, declaringType, inspector);
foreach (var field in declaringType.GetFields(flags))
if (field.DeclaringType == declaringType)
TryCacheMember(field, fields, cachedSigs, declaringType, inspector);
foreach (var method in declaringType.GetMethods(flags))
if (method.DeclaringType == declaringType)
TryCacheMember(method, methods, cachedSigs, declaringType, inspector);
}
var sorted = new List<CacheMember>();
sorted.AddRange(props.OrderBy(it => Array.IndexOf(types, it.DeclaringType))
.ThenBy(it => it.NameForFiltering));
sorted.AddRange(fields.OrderBy(it => Array.IndexOf(types, it.DeclaringType))
.ThenBy(it => it.NameForFiltering));
sorted.AddRange(ctors.OrderBy(it => Array.IndexOf(types, it.DeclaringType))
.ThenBy(it => it.NameForFiltering));
sorted.AddRange(methods.OrderBy(it => Array.IndexOf(types, it.DeclaringType))
.ThenBy(it => it.NameForFiltering));
return sorted;
}
private static void TryCacheMember(MemberInfo member, IList list, HashSet<string> cachedSigs,
Type declaringType, ReflectionInspector inspector, bool ignorePropertyMethodInfos = true)
{
try
{
if (UERuntimeHelper.IsBlacklisted(member))
return;
var sig = GetSig(member);
// ExplorerCore.Log($"Trying to cache member {sig}... ({member.MemberType})");
CacheMember cached;
Type returnType;
switch (member.MemberType)
{
case MemberTypes.Constructor:
{
var ci = member as ConstructorInfo;
sig += GetArgumentString(ci.GetParameters());
if (cachedSigs.Contains(sig))
return;
cached = new CacheConstructor(ci);
returnType = ci.DeclaringType;
}
break;
case MemberTypes.Method:
{
var mi = member as MethodInfo;
if (ignorePropertyMethodInfos
&& (mi.Name.StartsWith("get_") || mi.Name.StartsWith("set_")))
return;
sig += GetArgumentString(mi.GetParameters());
if (cachedSigs.Contains(sig))
return;
cached = new CacheMethod(mi);
returnType = mi.ReturnType;
break;
}
case MemberTypes.Property:
{
var pi = member as PropertyInfo;
if (!pi.CanRead && pi.CanWrite)
{
// write-only property, cache the set method instead.
var setMethod = pi.GetSetMethod(true);
if (setMethod != null)
TryCacheMember(setMethod, list, cachedSigs, declaringType, inspector, false);
return;
}
sig += GetArgumentString(pi.GetIndexParameters());
if (cachedSigs.Contains(sig))
return;
cached = new CacheProperty(pi);
returnType = pi.PropertyType;
break;
}
case MemberTypes.Field:
{
var fi = member as FieldInfo;
cached = new CacheField(fi);
returnType = fi.FieldType;
break;
}
default: return;
}
cachedSigs.Add(sig);
cached.SetFallbackType(returnType);
cached.SetInspectorOwner(inspector, member);
list.Add(cached);
}
catch (Exception e)
{
ExplorerCore.LogWarning($"Exception caching member {member.DeclaringType.FullName}.{member.Name}!");
ExplorerCore.Log(e.ToString());
}
}
internal static string GetSig(MemberInfo member)
=> $"{member.DeclaringType.Name}.{member.Name}";
internal static string GetArgumentString(ParameterInfo[] args)
{
var sb = new StringBuilder();
sb.Append(' ');
sb.Append('(');
foreach (var param in args)
{
sb.Append(param.ParameterType.Name);
sb.Append(' ');
sb.Append(param.Name);
sb.Append(',');
sb.Append(' ');
}
sb.Append(')');
return sb.ToString();
}
#endregion
}
}

View File

@ -0,0 +1,170 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityExplorer.Inspectors;
using UnityExplorer.Runtime;
using UniverseLib;
using HarmonyLib;
using HarmonyLib.Tools;
namespace UnityExplorer.CacheObject
{
public static class CacheMemberFactory
{
public static List<CacheMember> GetCacheMembers(Type type, ReflectionInspector inspector)
{
//var list = new List<CacheMember>();
HashSet<string> cachedSigs = new();
List<CacheMember> props = new();
List<CacheMember> fields = new();
List<CacheMember> ctors = new();
List<CacheMember> methods = new();
var types = ReflectionUtility.GetAllBaseTypes(type);
var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static;
if (!inspector.StaticOnly)
flags |= BindingFlags.Instance;
if (!type.IsAbstract)
{
// Get non-static constructors of the main type.
// There's no reason to get the static cctor, it will be invoked when we inspect the class.
// Also no point getting ctors on inherited types.
foreach (var ctor in type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
TryCacheMember(ctor, ctors, cachedSigs, type, inspector);
// structs always have a parameterless constructor
if (type.IsValueType)
{
CacheConstructor cached = new(type);
cached.SetFallbackType(type);
cached.SetInspectorOwner(inspector, null);
ctors.Add(cached);
}
}
foreach (var declaringType in types)
{
foreach (var prop in declaringType.GetProperties(flags))
if (prop.DeclaringType == declaringType)
TryCacheMember(prop, props, cachedSigs, declaringType, inspector);
foreach (var field in declaringType.GetFields(flags))
if (field.DeclaringType == declaringType)
TryCacheMember(field, fields, cachedSigs, declaringType, inspector);
foreach (var method in declaringType.GetMethods(flags))
if (method.DeclaringType == declaringType)
TryCacheMember(method, methods, cachedSigs, declaringType, inspector);
}
var sorted = new List<CacheMember>();
sorted.AddRange(props.OrderBy(it => Array.IndexOf(types, it.DeclaringType))
.ThenBy(it => it.NameForFiltering));
sorted.AddRange(fields.OrderBy(it => Array.IndexOf(types, it.DeclaringType))
.ThenBy(it => it.NameForFiltering));
sorted.AddRange(ctors.OrderBy(it => Array.IndexOf(types, it.DeclaringType))
.ThenBy(it => it.NameForFiltering));
sorted.AddRange(methods.OrderBy(it => Array.IndexOf(types, it.DeclaringType))
.ThenBy(it => it.NameForFiltering));
return sorted;
}
static void TryCacheMember<T>(MemberInfo member, List<T> list, HashSet<string> cachedSigs,
Type declaringType, ReflectionInspector inspector, bool ignorePropertyMethodInfos = true)
where T : CacheMember
{
try
{
if (UERuntimeHelper.IsBlacklisted(member))
return;
string sig = member switch
{
// method or constructor
MethodBase mb => mb.FullDescription(),
// property or field
PropertyInfo or FieldInfo => $"{member.DeclaringType.FullDescription()}.{member.Name}",
_ => throw new NotImplementedException(),
};
if (cachedSigs.Contains(sig))
return;
// ExplorerCore.Log($"Trying to cache member {sig}... ({member.MemberType})");
CacheMember cached;
Type returnType;
switch (member.MemberType)
{
case MemberTypes.Constructor:
{
var ci = member as ConstructorInfo;
cached = new CacheConstructor(ci);
returnType = ci.DeclaringType;
}
break;
case MemberTypes.Method:
{
var mi = member as MethodInfo;
if (ignorePropertyMethodInfos
&& (mi.Name.StartsWith("get_") || mi.Name.StartsWith("set_")))
return;
cached = new CacheMethod(mi);
returnType = mi.ReturnType;
break;
}
case MemberTypes.Property:
{
var pi = member as PropertyInfo;
if (!pi.CanRead && pi.CanWrite)
{
// write-only property, cache the set method instead.
var setMethod = pi.GetSetMethod(true);
if (setMethod != null)
TryCacheMember(setMethod, list, cachedSigs, declaringType, inspector, false);
return;
}
cached = new CacheProperty(pi);
returnType = pi.PropertyType;
break;
}
case MemberTypes.Field:
{
var fi = member as FieldInfo;
cached = new CacheField(fi);
returnType = fi.FieldType;
break;
}
default:
throw new NotImplementedException();
}
cachedSigs.Add(sig);
cached.SetFallbackType(returnType);
cached.SetInspectorOwner(inspector, member);
list.Add((T)cached);
}
catch (Exception e)
{
ExplorerCore.LogWarning($"Exception caching member {member.DeclaringType.FullName}.{member.Name}!");
ExplorerCore.Log(e);
}
}
}
}

View File

@ -45,13 +45,11 @@ namespace UnityExplorer.CacheObject
ret = methodInfo.Invoke(DeclaringInstance, Evaluator.TryParseArguments());
else
ret = methodInfo.Invoke(DeclaringInstance, ArgumentUtility.EmptyArgs);
HadException = false;
LastException = null;
return ret;
}
catch (Exception ex)
{
HadException = true;
LastException = ex;
return null;
}

View File

@ -57,7 +57,6 @@ namespace UnityExplorer.CacheObject
public abstract bool ShouldAutoEvaluate { get; }
public abstract bool HasArguments { get; }
public abstract bool CanWrite { get; }
public bool HadException { get; protected set; }
public Exception LastException { get; protected set; }
public virtual void SetFallbackType(Type fallbackType)
@ -106,8 +105,8 @@ namespace UnityExplorer.CacheObject
if (CellView != null)
SetDataToCell(CellView);
// If the owner's parent CacheObject is set, we are setting the value of an inspected struct.
// Set the inspector target as the value back to that parent cacheobject.
// If the owner's ParentCacheObject is set, we are setting the value of an inspected struct.
// Set the inspector target as the value back to that parent.
if (Owner.ParentCacheObject != null)
Owner.ParentCacheObject.SetUserValue(Owner.Target);
}
@ -137,7 +136,7 @@ namespace UnityExplorer.CacheObject
{
var prevState = State;
if (HadException)
if (LastException != null)
{
LastValueWasNull = true;
LastValueType = FallbackType;
@ -158,7 +157,7 @@ namespace UnityExplorer.CacheObject
{
// If we changed states (always needs IValue change)
// or if the value is null, and the fallback type isnt string (we always want to edit strings).
if (State != prevState || (State != ValueState.String && Value.IsNullOrDestroyed()))
if (State != prevState || (State != ValueState.String && State != ValueState.Exception && Value.IsNullOrDestroyed()))
{
// need to return IValue
ReleaseIValue();
@ -206,7 +205,7 @@ namespace UnityExplorer.CacheObject
return $"<i>{NOT_YET_EVAL} ({SignatureHighlighter.Parse(FallbackType, true)})</i>";
case ValueState.Exception:
return $"<i><color=red>{LastException.ReflectionExToString()}</color></i>";
return $"<i><color=#eb4034>{LastException.ReflectionExToString()}</color></i>";
// bool and number dont want the label for the value at all
case ValueState.Boolean:
@ -291,36 +290,36 @@ namespace UnityExplorer.CacheObject
switch (State)
{
case ValueState.Exception:
SetValueState(cell, ValueStateArgs.Default);
SetValueState(cell, new(true, subContentButtonActive: true));
break;
case ValueState.Boolean:
SetValueState(cell, new ValueStateArgs(false, toggleActive: true, applyActive: CanWrite));
SetValueState(cell, new(false, toggleActive: true, applyActive: CanWrite));
break;
case ValueState.Number:
SetValueState(cell, new ValueStateArgs(false, typeLabelActive: true, inputActive: true, applyActive: CanWrite));
SetValueState(cell, new(false, typeLabelActive: true, inputActive: true, applyActive: CanWrite));
break;
case ValueState.String:
if (LastValueWasNull)
SetValueState(cell, new ValueStateArgs(true, subContentButtonActive: true));
SetValueState(cell, new(true, subContentButtonActive: true));
else
SetValueState(cell, new ValueStateArgs(true, false, SignatureHighlighter.StringOrange, subContentButtonActive: true));
SetValueState(cell, new(true, false, SignatureHighlighter.StringOrange, subContentButtonActive: true));
break;
case ValueState.Enum:
SetValueState(cell, new ValueStateArgs(true, subContentButtonActive: CanWrite));
SetValueState(cell, new(true, subContentButtonActive: CanWrite));
break;
case ValueState.Color:
case ValueState.ValueStruct:
if (ParseUtility.CanParse(LastValueType))
SetValueState(cell, new ValueStateArgs(false, false, null, true, false, true, CanWrite, true, true));
SetValueState(cell, new(false, false, null, true, false, true, CanWrite, true, true));
else
SetValueState(cell, new ValueStateArgs(true, inspectActive: true, subContentButtonActive: true));
SetValueState(cell, new(true, inspectActive: true, subContentButtonActive: true));
break;
case ValueState.Collection:
case ValueState.Dictionary:
SetValueState(cell, new ValueStateArgs(true, inspectActive: !LastValueWasNull, subContentButtonActive: !LastValueWasNull));
SetValueState(cell, new(true, inspectActive: !LastValueWasNull, subContentButtonActive: !LastValueWasNull));
break;
case ValueState.Unsupported:
SetValueState(cell, new ValueStateArgs(true, inspectActive: !LastValueWasNull));
SetValueState(cell, new (true, inspectActive: !LastValueWasNull));
break;
}
@ -368,8 +367,10 @@ namespace UnityExplorer.CacheObject
if (cell.InspectButton != null)
cell.InspectButton.Component.gameObject.SetActive(args.inspectActive && !LastValueWasNull);
// allow IValue for null strings though
cell.SubContentButton.Component.gameObject.SetActive(args.subContentButtonActive && (!LastValueWasNull || State == ValueState.String));
// set subcontent button if needed, and for null strings and exceptions
cell.SubContentButton.Component.gameObject.SetActive(
args.subContentButtonActive
&& (!LastValueWasNull || State == ValueState.String || State == ValueState.Exception));
}
// CacheObjectCell Apply
@ -401,7 +402,7 @@ namespace UnityExplorer.CacheObject
{
if (this.IValue == null)
{
var ivalueType = InteractiveValue.GetIValueTypeForState(State);
Type ivalueType = InteractiveValue.GetIValueTypeForState(State);
if (ivalueType == null)
return;
@ -455,6 +456,7 @@ namespace UnityExplorer.CacheObject
{
inactiveIValueHolder = new GameObject("Temp_IValue_Holder");
GameObject.DontDestroyOnLoad(inactiveIValueHolder);
inactiveIValueHolder.hideFlags = HideFlags.HideAndDontSave;
inactiveIValueHolder.transform.parent = UniversalUI.PoolHolder.transform;
inactiveIValueHolder.SetActive(false);
}
@ -467,9 +469,20 @@ namespace UnityExplorer.CacheObject
public struct ValueStateArgs
{
public ValueStateArgs(bool valueActive = true, bool valueRichText = true, Color? valueColor = null,
bool typeLabelActive = false, bool toggleActive = false, bool inputActive = false, bool applyActive = false,
bool inspectActive = false, bool subContentButtonActive = false)
public static ValueStateArgs Default { get; } = new(true);
public Color valueColor;
public bool valueActive, valueRichText, typeLabelActive, toggleActive, inputActive, applyActive, inspectActive, subContentButtonActive;
public ValueStateArgs(bool valueActive = true,
bool valueRichText = true,
Color? valueColor = null,
bool typeLabelActive = false,
bool toggleActive = false,
bool inputActive = false,
bool applyActive = false,
bool inspectActive = false,
bool subContentButtonActive = false)
{
this.valueActive = valueActive;
this.valueRichText = valueRichText;
@ -481,14 +494,6 @@ namespace UnityExplorer.CacheObject
this.inspectActive = inspectActive;
this.subContentButtonActive = subContentButtonActive;
}
public static ValueStateArgs Default => _default;
private static ValueStateArgs _default = new ValueStateArgs(true);
public bool valueActive, valueRichText, typeLabelActive, toggleActive,
inputActive, applyActive, inspectActive, subContentButtonActive;
public Color valueColor;
}
}
}

View File

@ -40,13 +40,11 @@ namespace UnityExplorer.CacheObject
ret = PropertyInfo.GetValue(DeclaringInstance, this.Evaluator.TryParseArguments());
else
ret = PropertyInfo.GetValue(DeclaringInstance, null);
HadException = false;
LastException = null;
return ret;
}
catch (Exception ex)
{
HadException = true;
LastException = ex;
return null;
}

View File

@ -29,8 +29,11 @@ namespace UnityExplorer.CacheObject.IValues
private EnumCompleter enumCompleter;
private GameObject toggleHolder;
private readonly List<Toggle> flagToggles = new List<Toggle>();
private readonly List<Text> flagTexts = new List<Text>();
private readonly List<Toggle> flagToggles = new();
private readonly List<Text> flagTexts = new();
public CachedEnumValue ValueAtIndex(int idx) => (CachedEnumValue)CurrentValues[idx];
public CachedEnumValue ValueAtKey(object key) => (CachedEnumValue)CurrentValues[key];
// Setting value from owner
public override void SetValue(object value)
@ -70,13 +73,8 @@ namespace UnityExplorer.CacheObject.IValues
{
try
{
var split = value.ToString().Split(',');
var set = new HashSet<string>();
foreach (var s in split)
set.Add(s.Trim());
for (int i = 0; i < CurrentValues.Count; i++)
flagToggles[i].isOn = set.Contains(ValueAtIdx(i).Name);
flagToggles[i].isOn = (value as Enum).HasFlag(ValueAtIndex(i).ActualValue as Enum);
}
catch (Exception ex)
{
@ -116,7 +114,7 @@ namespace UnityExplorer.CacheObject.IValues
for (int i = 0; i < CurrentValues.Count; i++)
{
if (flagToggles[i].isOn)
values.Add(ValueAtIdx(i).Name);
values.Add(ValueAtIndex(i).Name);
}
CurrentOwner.SetUserValue(Enum.Parse(EnumType, string.Join(", ", values.ToArray())));
@ -166,9 +164,6 @@ namespace UnityExplorer.CacheObject.IValues
return UIRoot;
}
public CachedEnumValue ValueAtIdx(int idx) => (CachedEnumValue)CurrentValues[idx];
public CachedEnumValue ValueAtKey(object key) => (CachedEnumValue)CurrentValues[key];
private void SetupTogglesForEnumType()
{
toggleHolder.SetActive(true);
@ -191,7 +186,7 @@ namespace UnityExplorer.CacheObject.IValues
AddToggleRow();
flagToggles[i].isOn = false;
flagTexts[i].text = ValueAtIdx(i).Name;
flagTexts[i].text = ValueAtIndex(i).Name;
}
}

View File

@ -31,8 +31,9 @@ namespace UnityExplorer.CacheObject.IValues
{
base.OnBorrowed(owner);
inputField.Component.readOnly = !owner.CanWrite;
ApplyButton.Component.gameObject.SetActive(owner.CanWrite);
bool canWrite = owner.CanWrite && owner.State != ValueState.Exception;
inputField.Component.readOnly = !canWrite;
ApplyButton.Component.gameObject.SetActive(canWrite);
SaveFilePath.Text = Path.Combine(ConfigManager.Default_Output_Path.Value, "untitled.txt");
}
@ -47,6 +48,9 @@ namespace UnityExplorer.CacheObject.IValues
public override void SetValue(object value)
{
if (CurrentOwner.State == ValueState.Exception)
value = CurrentOwner.LastException.ToString();
RealValue = value as string;
SaveFileRow.SetActive(IsStringTooLong(RealValue));

View File

@ -16,22 +16,16 @@ namespace UnityExplorer.CacheObject.IValues
{
public static Type GetIValueTypeForState(ValueState state)
{
switch (state)
return state switch
{
case ValueState.String:
return typeof(InteractiveString);
case ValueState.Enum:
return typeof(InteractiveEnum);
case ValueState.Collection:
return typeof(InteractiveList);
case ValueState.Dictionary:
return typeof(InteractiveDictionary);
case ValueState.ValueStruct:
return typeof(InteractiveValueStruct);
case ValueState.Color:
return typeof(InteractiveColor);
default: return null;
}
ValueState.Exception or ValueState.String => typeof(InteractiveString),
ValueState.Enum => typeof(InteractiveEnum),
ValueState.Collection => typeof(InteractiveList),
ValueState.Dictionary => typeof(InteractiveDictionary),
ValueState.ValueStruct => typeof(InteractiveValueStruct),
ValueState.Color => typeof(InteractiveColor),
_ => null,
};
}
public GameObject UIRoot { get; set; }
@ -39,28 +33,28 @@ namespace UnityExplorer.CacheObject.IValues
public virtual bool CanWrite => this.CurrentOwner.CanWrite;
public CacheObjectBase CurrentOwner => m_owner;
private CacheObjectBase m_owner;
public CacheObjectBase CurrentOwner => owner;
private CacheObjectBase owner;
public bool PendingValueWanted;
public virtual void OnBorrowed(CacheObjectBase owner)
{
if (this.m_owner != null)
if (this.owner != null)
{
ExplorerCore.LogWarning("Setting an IValue's owner but there is already one set. Maybe it wasn't cleaned up?");
ReleaseFromOwner();
}
this.m_owner = owner;
this.owner = owner;
}
public virtual void ReleaseFromOwner()
{
if (this.m_owner == null)
if (this.owner == null)
return;
this.m_owner = null;
this.owner = null;
}
public abstract void SetValue(object value);

View File

@ -9,29 +9,26 @@ using UnhollowerRuntimeLib;
namespace UnityExplorer
{
// Handles all Behaviour update calls for UnityExplorer (Update, FixedUpdate, OnPostRender).
// Basically just a wrapper which calls the corresponding methods in ExplorerCore.
public class ExplorerBehaviour : MonoBehaviour
{
internal static ExplorerBehaviour Instance { get; private set; }
#if CPP
public ExplorerBehaviour(IntPtr ptr) : base(ptr) { }
#endif
internal static void Setup()
{
#if CPP
ClassInjector.RegisterTypeInIl2Cpp<ExplorerBehaviour>();
#endif
var obj = new GameObject("ExplorerBehaviour");
GameObject.DontDestroyOnLoad(obj);
obj.hideFlags |= HideFlags.HideAndDontSave;
GameObject obj = new("ExplorerBehaviour");
DontDestroyOnLoad(obj);
obj.hideFlags = HideFlags.HideAndDontSave;
Instance = obj.AddComponent<ExplorerBehaviour>();
}
#if CPP
public ExplorerBehaviour(IntPtr ptr) : base(ptr) { }
#endif
internal void Update()
{
ExplorerCore.Update();

View File

@ -1,24 +1,21 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
using UnityExplorer.CacheObject;
using UnityExplorer.Config;
using UnityExplorer.UI;
using UnityExplorer.ObjectExplorer;
using UnityExplorer.UI.Panels;
using UnityExplorer.Runtime;
using UnityExplorer.UI;
using UnityExplorer.UI.Panels;
using UniverseLib;
using UniverseLib.Input;
using UniverseLib.UI;
using System.Reflection;
namespace UnityExplorer
{
public static class ExplorerCore
{
public const string NAME = "UnityExplorer";
public const string VERSION = "4.5.11";
public const string VERSION = "4.6.3";
public const string AUTHOR = "Sinai";
public const string GUID = "com.sinai.unityexplorer";
@ -32,23 +29,20 @@ namespace UnityExplorer
public static void Init(IExplorerLoader loader)
{
if (Loader != null)
{
LogWarning("UnityExplorer is already loaded!");
return;
}
throw new Exception("UnityExplorer is already loaded.");
Loader = loader;
Log($"{NAME} {VERSION} initializing...");
if (!Directory.Exists(Loader.ExplorerFolder))
Directory.CreateDirectory(Loader.ExplorerFolder);
Directory.CreateDirectory(Loader.ExplorerFolder);
ConfigManager.Init(Loader.ConfigHandler);
UERuntimeHelper.Init();
ExplorerBehaviour.Setup();
UnityCrashPrevention.Init();
UniverseLib.Universe.Init(ConfigManager.Startup_Delay_Time.Value, LateInit, Log, new()
Universe.Init(ConfigManager.Startup_Delay_Time.Value, LateInit, Log, new()
{
Disable_EventSystem_Override = ConfigManager.Disable_EventSystem_Override.Value,
Force_Unlock_Mouse = ConfigManager.Force_Unlock_Mouse.Value,
@ -59,7 +53,7 @@ namespace UnityExplorer
// Do a delayed setup so that objects aren't destroyed instantly.
// This can happen for a multitude of reasons.
// Default delay is 1 second which is usually enough.
private static void LateInit()
static void LateInit()
{
Log($"Setting up late core features...");
@ -69,21 +63,18 @@ namespace UnityExplorer
UIManager.InitUI();
Log($"{NAME} {VERSION} initialized for {UniverseLib.Universe.Context}.");
Log($"{NAME} {VERSION} ({Universe.Context}) initialized.");
//InspectorManager.Inspect(typeof(Tests.TestClass));
}
/// <summary>
/// Should be called once per frame.
/// </summary>
public static void Update()
internal static void Update()
{
UIManager.Update();
// check master toggle
if (InputManager.GetKeyDown(ConfigManager.Master_Toggle.Value))
UIManager.ShowMenu = !UIManager.ShowMenu;
UIManager.Update();
}
#region LOGGING

View File

@ -2,29 +2,34 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityExplorer.Config;
using UnityExplorer.Runtime;
using UnityExplorer.CacheObject;
using UnityExplorer.CacheObject.Views;
using UnityExplorer.UI.Panels;
using UnityExplorer.UI.Widgets;
using UnityExplorer.UI;
using UniverseLib.UI.Widgets;
using UniverseLib.UI;
using UniverseLib;
using UniverseLib.Runtime;
using UniverseLib.UI;
using UniverseLib.UI.Models;
using UniverseLib.UI.ObjectPool;
using UniverseLib.UI.Widgets.ScrollView;
using UniverseLib.Utility;
namespace UnityExplorer.Inspectors
{
[Flags]
public enum MemberFilter
{
None = 0,
Property = 1,
Field = 2,
Constructor = 4,
Method = 8,
All = Property | Field | Method | Constructor,
}
public class ReflectionInspector : InspectorBase, ICellPoolDataSource<CacheMemberCell>, ICacheObjectController
{
public CacheObjectBase ParentCacheObject { get; set; }
@ -32,44 +37,52 @@ namespace UnityExplorer.Inspectors
public bool StaticOnly { get; internal set; }
public bool CanWrite => true;
public bool AutoUpdateWanted => autoUpdateToggle.isOn;
private List<CacheMember> members = new();
private readonly List<CacheMember> filteredMembers = new();
public bool AutoUpdateWanted => autoUpdateToggle.isOn;
private BindingFlags scopeFlagsFilter;
private string nameFilter;
private BindingFlags FlagsFilter;
private string NameFilter;
private MemberFilter MemberFilter = MemberFilter.All;
private MemberFlags MemberFilter = MemberFlags.All;
private enum MemberFlags
{
None = 0,
Property = 1,
Field = 2,
Constructor = 4,
Method = 8,
All = Property | Field | Method | Constructor,
}
// Updating
private bool refreshWanted;
private string lastNameFilter;
private BindingFlags lastFlagsFilter;
private MemberFilter lastMemberFilter = MemberFilter.All;
private float timeOfLastAutoUpdate;
// UI
internal GameObject mainContentHolder;
private static int LeftGroupWidth { get; set; }
private static int RightGroupWidth { get; set; }
public ScrollPool<CacheMemberCell> MemberScrollPool { get; private set; }
public int ItemCount => filteredMembers.Count;
public UnityObjectWidget UnityWidget;
public InputFieldRef HiddenNameText;
public Text NameText;
public Text AssemblyText;
private Toggle autoUpdateToggle;
private string currentBaseTabText;
private readonly Color disabledButtonColor = new(0.24f, 0.24f, 0.24f);
private readonly Color enabledButtonColor = new(0.2f, 0.27f, 0.2f);
internal string currentBaseTabText;
private readonly Dictionary<BindingFlags, ButtonRef> scopeFilterButtons = new();
private readonly List<Toggle> memberTypeToggles = new();
private InputFieldRef filterInputField;
// Setup / return
// const
private readonly Color disabledButtonColor = new(0.24f, 0.24f, 0.24f);
private readonly Color enabledButtonColor = new(0.2f, 0.27f, 0.2f);
// Setup
public override void OnBorrowedFromPool(object target)
{
@ -105,10 +118,12 @@ namespace UnityExplorer.Inspectors
autoUpdateToggle.isOn = false;
UnityObjectRef = null;
ComponentRef = null;
TextureRef = null;
CleanupTextureViewer();
if (UnityWidget != null)
{
UnityWidget.OnReturnToPool();
Pool.Return(UnityWidget.GetType(), UnityWidget);
this.UnityWidget = null;
}
base.OnReturnToPool();
}
@ -143,18 +158,20 @@ namespace UnityExplorer.Inspectors
asmText = Path.GetFileName(TargetType.Assembly.Location);
AssemblyText.text = $"<color=grey>Assembly:</color> {asmText}";
// unity helpers
SetUnityTargets();
// Unity object helper widget
if (!StaticOnly)
this.UnityWidget = UnityObjectWidget.GetUnityWidget(target, TargetType, this);
// Get cache members
this.members = CacheMember.GetCacheMembers(Target, TargetType, this);
this.members = CacheMemberFactory.GetCacheMembers(TargetType, this);
// reset filters
this.filterInputField.Text = "";
this.filterInputField.Text = string.Empty;
SetFilter("", StaticOnly ? BindingFlags.Static : BindingFlags.Default);
SetFilter(string.Empty, StaticOnly ? BindingFlags.Static : BindingFlags.Default);
scopeFilterButtons[BindingFlags.Default].Component.gameObject.SetActive(!StaticOnly);
scopeFilterButtons[BindingFlags.Instance].Component.gameObject.SetActive(!StaticOnly);
@ -166,12 +183,6 @@ namespace UnityExplorer.Inspectors
// Updating
private bool refreshWanted;
private string lastNameFilter;
private BindingFlags lastFlagsFilter;
private MemberFlags lastMemberFilter = MemberFlags.All;
private float timeOfLastAutoUpdate;
public override void Update()
{
if (!this.IsActive)
@ -184,10 +195,10 @@ namespace UnityExplorer.Inspectors
}
// check filter changes or force-refresh
if (refreshWanted || NameFilter != lastNameFilter || FlagsFilter != lastFlagsFilter || lastMemberFilter != MemberFilter)
if (refreshWanted || nameFilter != lastNameFilter || scopeFlagsFilter != lastFlagsFilter || lastMemberFilter != MemberFilter)
{
lastNameFilter = NameFilter;
lastFlagsFilter = FlagsFilter;
lastNameFilter = nameFilter;
lastFlagsFilter = scopeFlagsFilter;
lastMemberFilter = MemberFilter;
FilterMembers();
@ -200,11 +211,8 @@ namespace UnityExplorer.Inspectors
{
timeOfLastAutoUpdate = Time.realtimeSinceStartup;
if (this.UnityObjectRef)
{
nameInput.Text = UnityObjectRef.name;
this.Tab.TabText.text = $"{currentBaseTabText} \"{UnityObjectRef.name}\"";
}
if (this.UnityWidget != null)
UnityWidget.Update();
if (AutoUpdateWanted)
UpdateDisplayedMembers();
@ -218,26 +226,26 @@ namespace UnityExplorer.Inspectors
// Filtering
public void SetFilter(string filter) => SetFilter(filter, FlagsFilter);
public void SetFilter(string name) => SetFilter(name, scopeFlagsFilter);
public void SetFilter(BindingFlags flagsFilter) => SetFilter(NameFilter, flagsFilter);
public void SetFilter(BindingFlags flags) => SetFilter(nameFilter, flags);
public void SetFilter(string nameFilter, BindingFlags flagsFilter)
public void SetFilter(string name, BindingFlags flags)
{
this.NameFilter = nameFilter;
this.nameFilter = name;
if (flagsFilter != FlagsFilter)
if (flags != scopeFlagsFilter)
{
var btn = scopeFilterButtons[FlagsFilter].Component;
var btn = scopeFilterButtons[scopeFlagsFilter].Component;
RuntimeHelper.SetColorBlock(btn, disabledButtonColor, disabledButtonColor * 1.3f);
this.FlagsFilter = flagsFilter;
btn = scopeFilterButtons[FlagsFilter].Component;
this.scopeFlagsFilter = flags;
btn = scopeFilterButtons[scopeFlagsFilter].Component;
RuntimeHelper.SetColorBlock(btn, enabledButtonColor, enabledButtonColor * 1.3f);
}
}
private void OnMemberTypeToggled(MemberFlags flag, bool val)
private void OnMemberTypeToggled(MemberFilter flag, bool val)
{
if (!val)
MemberFilter &= ~flag;
@ -253,19 +261,20 @@ namespace UnityExplorer.Inspectors
{
var member = members[i];
if (FlagsFilter != BindingFlags.Default)
if (scopeFlagsFilter != BindingFlags.Default)
{
if (FlagsFilter == BindingFlags.Instance && member.IsStatic
|| FlagsFilter == BindingFlags.Static && !member.IsStatic)
if (scopeFlagsFilter == BindingFlags.Instance && member.IsStatic
|| scopeFlagsFilter == BindingFlags.Static && !member.IsStatic)
continue;
}
if ((member is CacheMethod && !MemberFilter.HasFlag(MemberFlags.Method))
|| (member is CacheField && !MemberFilter.HasFlag(MemberFlags.Field))
|| (member is CacheProperty && !MemberFilter.HasFlag(MemberFlags.Property)))
if ((member is CacheMethod && !MemberFilter.HasFlag(MemberFilter.Method))
|| (member is CacheField && !MemberFilter.HasFlag(MemberFilter.Field))
|| (member is CacheProperty && !MemberFilter.HasFlag(MemberFilter.Property))
|| (member is CacheConstructor && !MemberFilter.HasFlag(MemberFilter.Constructor)))
continue;
if (!string.IsNullOrEmpty(NameFilter) && !member.NameForFiltering.ContainsIgnoreCase(NameFilter))
if (!string.IsNullOrEmpty(nameFilter) && !member.NameForFiltering.ContainsIgnoreCase(nameFilter))
continue;
filteredMembers.Add(member);
@ -294,8 +303,6 @@ namespace UnityExplorer.Inspectors
// Member cells
public int ItemCount => filteredMembers.Count;
public void OnCellBorrowed(CacheMemberCell cell) { } // not needed
public void SetCell(CacheMemberCell cell, int index)
@ -305,9 +312,6 @@ namespace UnityExplorer.Inspectors
// Cell layout (fake table alignment)
private static int LeftGroupWidth { get; set; }
private static int RightGroupWidth { get; set; }
internal void SetLayouts()
{
CalculateLayouts();
@ -338,8 +342,6 @@ namespace UnityExplorer.Inspectors
// UI Construction
private GameObject mainContentHolder;
public override GameObject CreateContent(GameObject parent)
{
UIRoot = UIFactory.CreateVerticalGroup(parent, "ReflectionInspector", true, true, true, true, 5,
@ -380,8 +382,6 @@ namespace UnityExplorer.Inspectors
AssemblyText = UIFactory.CreateLabel(UIRoot, "AssemblyLabel", "not set", TextAnchor.MiddleLeft);
UIFactory.SetLayoutElement(AssemblyText.gameObject, minHeight: 25, flexibleWidth: 9999);
ConstructUnityObjectRow();
mainContentHolder = UIFactory.CreateVerticalGroup(UIRoot, "MemberHolder", false, false, true, true, 5, new Vector4(2, 2, 2, 2),
new Color(0.12f, 0.12f, 0.12f));
UIFactory.SetLayoutElement(mainContentHolder, flexibleWidth: 9999, flexibleHeight: 9999);
@ -494,12 +494,12 @@ namespace UnityExplorer.Inspectors
toggle.graphic.TryCast<Image>().color = color.ToColor() * 0.65f;
MemberFlags flag = type switch
MemberFilter flag = type switch
{
MemberTypes.Method => MemberFlags.Method,
MemberTypes.Property => MemberFlags.Property,
MemberTypes.Field => MemberFlags.Field,
MemberTypes.Constructor => MemberFlags.Constructor,
MemberTypes.Method => MemberFilter.Method,
MemberTypes.Property => MemberFilter.Property,
MemberTypes.Field => MemberFilter.Field,
MemberTypes.Constructor => MemberFilter.Constructor,
_ => throw new NotImplementedException()
};
@ -507,237 +507,5 @@ namespace UnityExplorer.Inspectors
memberTypeToggles.Add(toggle);
}
// Todo should probably put this in a separate class or maybe as a widget
#region UNITY OBJECT SPECIFIC
// Unity object helpers
private UnityEngine.Object UnityObjectRef;
private Component ComponentRef;
private Texture2D TextureRef;
private bool TextureViewerWanted;
private GameObject unityObjectRow;
private ButtonRef gameObjectButton;
private InputFieldRef nameInput;
private InputFieldRef instanceIdInput;
private ButtonRef textureButton;
private GameObject textureViewer;
private void SetUnityTargets()
{
if (StaticOnly || !typeof(UnityEngine.Object).IsAssignableFrom(TargetType))
{
unityObjectRow.SetActive(false);
textureViewer.SetActive(false);
return;
}
UnityObjectRef = (UnityEngine.Object)Target.TryCast(typeof(UnityEngine.Object));
unityObjectRow.SetActive(true);
nameInput.Text = UnityObjectRef.name;
instanceIdInput.Text = UnityObjectRef.GetInstanceID().ToString();
if (typeof(Component).IsAssignableFrom(TargetType))
{
ComponentRef = (Component)Target.TryCast(typeof(Component));
gameObjectButton.Component.gameObject.SetActive(true);
}
else
gameObjectButton.Component.gameObject.SetActive(false);
if (typeof(Texture2D).IsAssignableFrom(TargetType))
{
TextureRef = (Texture2D)Target.TryCast(typeof(Texture2D));
textureButton.Component.gameObject.SetActive(true);
}
else
textureButton.Component.gameObject.SetActive(false);
}
private void OnGameObjectButtonClicked()
{
if (!ComponentRef)
{
ExplorerCore.LogWarning("Component reference is null or destroyed!");
return;
}
InspectorManager.Inspect(ComponentRef.gameObject);
}
private void ToggleTextureViewer()
{
if (TextureViewerWanted)
{
// disable
TextureViewerWanted = false;
textureViewer.SetActive(false);
mainContentHolder.SetActive(true);
textureButton.ButtonText.text = "View Texture";
}
else
{
if (!textureImage.sprite)
{
// First show, need to create sprite for displaying texture
SetTextureViewer();
}
// enable
TextureViewerWanted = true;
textureViewer.SetActive(true);
mainContentHolder.gameObject.SetActive(false);
textureButton.ButtonText.text = "Hide Texture";
}
}
// UI construction
private void ConstructUnityObjectRow()
{
unityObjectRow = UIFactory.CreateUIObject("UnityObjectRow", UIRoot);
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(unityObjectRow, false, false, true, true, 5);
UIFactory.SetLayoutElement(unityObjectRow, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999);
textureButton = UIFactory.CreateButton(unityObjectRow, "TextureButton", "View Texture", new Color(0.2f, 0.2f, 0.2f));
UIFactory.SetLayoutElement(textureButton.Component.gameObject, minHeight: 25, minWidth: 150);
textureButton.OnClick += ToggleTextureViewer;
var nameLabel = UIFactory.CreateLabel(unityObjectRow, "NameLabel", "Name:", TextAnchor.MiddleLeft, Color.grey);
UIFactory.SetLayoutElement(nameLabel.gameObject, minHeight: 25, minWidth: 45, flexibleWidth: 0);
nameInput = UIFactory.CreateInputField(unityObjectRow, "NameInput", "untitled");
UIFactory.SetLayoutElement(nameInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 1000);
nameInput.Component.readOnly = true;
gameObjectButton = UIFactory.CreateButton(unityObjectRow, "GameObjectButton", "Inspect GameObject", new Color(0.2f, 0.2f, 0.2f));
UIFactory.SetLayoutElement(gameObjectButton.Component.gameObject, minHeight: 25, minWidth: 160);
gameObjectButton.OnClick += OnGameObjectButtonClicked;
var instanceLabel = UIFactory.CreateLabel(unityObjectRow, "InstanceLabel", "Instance ID:", TextAnchor.MiddleRight, Color.grey);
UIFactory.SetLayoutElement(instanceLabel.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0);
instanceIdInput = UIFactory.CreateInputField(unityObjectRow, "InstanceIDInput", "ERROR");
UIFactory.SetLayoutElement(instanceIdInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 0);
instanceIdInput.Component.readOnly = true;
unityObjectRow.SetActive(false);
ConstructTextureHelper();
}
// Texture viewer helper
private InputFieldRef textureSavePathInput;
private Image textureImage;
private LayoutElement textureImageLayout;
private void CleanupTextureViewer()
{
if (textureImage.sprite)
GameObject.Destroy(textureImage.sprite);
if (TextureViewerWanted)
ToggleTextureViewer();
}
private void ConstructTextureHelper()
{
textureViewer = UIFactory.CreateVerticalGroup(UIRoot, "TextureViewer", false, false, true, true, 2, new Vector4(5, 5, 5, 5),
new Color(0.1f, 0.1f, 0.1f));
UIFactory.SetLayoutElement(textureViewer, flexibleWidth: 9999, flexibleHeight: 9999);
// Save helper
var saveRowObj = UIFactory.CreateHorizontalGroup(textureViewer, "SaveRow", false, false, true, true, 2, new Vector4(2, 2, 2, 2),
new Color(0.1f, 0.1f, 0.1f));
var saveBtn = UIFactory.CreateButton(saveRowObj, "SaveButton", "Save .PNG", new Color(0.2f, 0.25f, 0.2f));
UIFactory.SetLayoutElement(saveBtn.Component.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0);
saveBtn.OnClick += OnSaveTextureClicked;
textureSavePathInput = UIFactory.CreateInputField(saveRowObj, "SaveInput", "...");
UIFactory.SetLayoutElement(textureSavePathInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 9999);
// Actual texture viewer
var imageViewport = UIFactory.CreateVerticalGroup(textureViewer, "Viewport", false, false, true, true);
imageViewport.GetComponent<Image>().color = Color.white;
imageViewport.AddComponent<Mask>().showMaskGraphic = false;
var imageObj = UIFactory.CreateUIObject("Image", imageViewport);
var fitter = imageObj.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
textureImage = imageObj.AddComponent<Image>();
textureImageLayout = UIFactory.SetLayoutElement(imageObj, flexibleWidth: 1, flexibleHeight: 1);
textureViewer.SetActive(false);
}
private void SetTextureViewer()
{
if (!this.TextureRef)
return;
var name = TextureRef.name;
if (string.IsNullOrEmpty(name))
name = "untitled";
textureSavePathInput.Text = Path.Combine(ConfigManager.Default_Output_Path.Value, $"{name}.png");
var sprite = TextureHelper.CreateSprite(TextureRef);
textureImage.sprite = sprite;
textureImageLayout.preferredHeight = sprite.rect.height;
// not really working, its always stretched horizontally for some reason.
textureImageLayout.preferredWidth = sprite.rect.width;
}
private void OnSaveTextureClicked()
{
if (!TextureRef)
{
ExplorerCore.LogWarning("Ref Texture is null, maybe it was destroyed?");
return;
}
if (string.IsNullOrEmpty(textureSavePathInput.Text))
{
ExplorerCore.LogWarning("Save path cannot be empty!");
return;
}
var path = textureSavePathInput.Text;
if (!path.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase))
{
ExplorerCore.LogWarning("Desired save path must end with '.png'!");
return;
}
path = IOUtility.EnsureValidFilePath(path);
if (File.Exists(path))
File.Delete(path);
var tex = TextureRef;
if (!TextureHelper.IsReadable(tex))
tex = TextureHelper.ForceReadTexture(tex);
byte[] data = TextureHelper.EncodeToPNG(tex);
File.WriteAllBytes(path, data);
if (tex != TextureRef)
{
// cleanup temp texture if we had to force-read it.
GameObject.Destroy(tex);
}
}
#endregion
}
}

View File

@ -0,0 +1,33 @@
#if STANDALONE
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
namespace UnityExplorer.Loader.Standalone
{
public class ExplorerEditorBehaviour : MonoBehaviour
{
internal void Awake()
{
ExplorerEditorLoader.Initialize();
DontDestroyOnLoad(this);
this.gameObject.hideFlags = HideFlags.HideAndDontSave;
}
internal void OnDestroy()
{
OnApplicationQuit();
}
internal void OnApplicationQuit()
{
if (UI.UIManager.UIRoot)
Destroy(UI.UIManager.UIRoot.transform.root.gameObject);
}
}
}
#endif

View File

@ -0,0 +1,41 @@
#if STANDALONE
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine;
namespace UnityExplorer.Loader.Standalone
{
public class ExplorerEditorLoader : ExplorerStandalone
{
public static void Initialize()
{
Instance = new ExplorerEditorLoader();
OnLog += LogHandler;
Instance.configHandler = new StandaloneConfigHandler();
ExplorerCore.Init(Instance);
}
static void LogHandler(string message, LogType logType)
{
switch (logType)
{
case LogType.Assert: Debug.LogError(message); break;
case LogType.Error: Debug.LogError(message); break;
case LogType.Exception: Debug.LogError(message); break;
case LogType.Log: Debug.Log(message); break;
case LogType.Warning: Debug.LogWarning(message); break;
}
}
protected override void CheckExplorerFolder()
{
if (explorerFolder == null)
explorerFolder = Path.Combine(Application.dataPath, "UnityExplorer~");
}
}
}
#endif

View File

@ -5,9 +5,9 @@ using System.IO;
using System.Reflection;
using UnityEngine;
using UnityExplorer.Config;
using UnityExplorer.Loader.STANDALONE;
using UnityEngine.EventSystems;
using UniverseLib.Input;
using UnityExplorer.Loader.Standalone;
#if CPP
using UnhollowerRuntimeLib;
#endif
@ -16,6 +16,33 @@ namespace UnityExplorer
{
public class ExplorerStandalone : IExplorerLoader
{
public static ExplorerStandalone Instance { get; protected set; }
/// <summary>
/// Invoked whenever Explorer logs something. Subscribe to this to handle logging.
/// </summary>
public static event Action<string, LogType> OnLog;
public string UnhollowedModulesFolder => unhollowedPath;
private string unhollowedPath;
public ConfigHandler ConfigHandler => configHandler;
internal StandaloneConfigHandler configHandler;
public string ExplorerFolder
{
get
{
CheckExplorerFolder();
return explorerFolder;
}
}
protected static string explorerFolder;
Action<object> IExplorerLoader.OnLogMessage => (object log) => { OnLog?.Invoke(log?.ToString() ?? "", LogType.Log); };
Action<object> IExplorerLoader.OnLogWarning => (object log) => { OnLog?.Invoke(log?.ToString() ?? "", LogType.Warning); };
Action<object> IExplorerLoader.OnLogError => (object log) => { OnLog?.Invoke(log?.ToString() ?? "", LogType.Error); };
/// <summary>
/// Call this to initialize UnityExplorer without adding a log listener or Unhollowed modules path.
/// The default Unhollowed path "UnityExplorer\Modules\" will be used.
@ -50,60 +77,33 @@ namespace UnityExplorer
OnLog += logListener;
if (string.IsNullOrEmpty(unhollowedModulesPath) || !Directory.Exists(unhollowedModulesPath))
instance._unhollowedPath = Path.Combine(instance.ExplorerFolder, "Modules");
instance.unhollowedPath = Path.Combine(instance.ExplorerFolder, "Modules");
else
instance._unhollowedPath = unhollowedModulesPath;
instance.unhollowedPath = unhollowedModulesPath;
return instance;
}
public static ExplorerStandalone Instance { get; private set; }
/// <summary>
/// Invoked whenever Explorer logs something. Subscribe to this to handle logging.
/// </summary>
public static event Action<string, LogType> OnLog;
public string UnhollowedModulesFolder => _unhollowedPath;
private string _unhollowedPath;
public ConfigHandler ConfigHandler => _configHandler;
private StandaloneConfigHandler _configHandler;
public string ExplorerFolder
{
get
{
CheckExplorerFolder();
return s_explorerFolder;
}
}
private static string s_explorerFolder;
Action<object> IExplorerLoader.OnLogMessage => (object log) => { OnLog?.Invoke(log?.ToString() ?? "", LogType.Log); };
Action<object> IExplorerLoader.OnLogWarning => (object log) => { OnLog?.Invoke(log?.ToString() ?? "", LogType.Warning); };
Action<object> IExplorerLoader.OnLogError => (object log) => { OnLog?.Invoke(log?.ToString() ?? "", LogType.Error); };
private void Init()
internal void Init()
{
Instance = this;
_configHandler = new StandaloneConfigHandler();
configHandler = new StandaloneConfigHandler();
ExplorerCore.Init(this);
}
private void CheckExplorerFolder()
protected virtual void CheckExplorerFolder()
{
if (s_explorerFolder == null)
if (explorerFolder == null)
{
s_explorerFolder =
explorerFolder =
Path.Combine(
Path.GetDirectoryName(
Uri.UnescapeDataString(new Uri(Assembly.GetExecutingAssembly().CodeBase).AbsolutePath)),
"UnityExplorer");
if (!Directory.Exists(s_explorerFolder))
Directory.CreateDirectory(s_explorerFolder);
if (!Directory.Exists(explorerFolder))
Directory.CreateDirectory(explorerFolder);
}
}
}

View File

@ -9,7 +9,7 @@ using UnityEngine;
using Tomlet;
using Tomlet.Models;
namespace UnityExplorer.Loader.STANDALONE
namespace UnityExplorer.Loader.Standalone
{
public class StandaloneConfigHandler : ConfigHandler
{
@ -73,7 +73,11 @@ namespace UnityExplorer.Loader.STANDALONE
return bool.Parse(value);
else if (elementType == typeof(int))
return int.Parse(value);
else
else if (elementType == typeof(float))
return float.Parse(value);
else if (elementType.IsEnum)
return Enum.Parse(elementType, value);
else
return value;
}

View File

@ -26,7 +26,7 @@ namespace UnityExplorer.ObjectExplorer
private static Scene? selectedScene;
/// <summary>The GameObjects in the currently inspected scene.</summary>
public static GameObject[] CurrentRootObjects { get; private set; } = new GameObject[0];
public static IEnumerable<GameObject> CurrentRootObjects { get; private set; } = new GameObject[0];
/// <summary>All currently loaded Scenes.</summary>
public static List<Scene> LoadedScenes { get; private set; } = new();
@ -129,7 +129,7 @@ namespace UnityExplorer.ObjectExplorer
if (go.transform.parent == null && !go.scene.IsValid())
objects.Add(go);
}
CurrentRootObjects = objects.ToArray();
CurrentRootObjects = objects;
}
}
}

View File

@ -24,8 +24,12 @@ namespace UnityExplorer.Tests
#endif
}
#region MONO
public static object LiterallyAnything = null;
public static string Exception => throw new Exception("This is a test.");
// Test enumerables
public static int[,,] MultiDimensionalArray = new int[45, 45, 45];
public static List<object> ListOfInts;
@ -90,12 +94,12 @@ namespace UnityExplorer.Tests
ExplorerCore.Log($"Test3 {typeof(T).FullName}");
}
public static void TestArgumentParse(string _string,
int integer,
Color color,
CameraClearFlags flags,
Vector3 vector,
Quaternion quaternion,
public static void TestArgumentParse(string _string,
int integer,
Color color,
CameraClearFlags flags,
Vector3 vector,
Quaternion quaternion,
object obj,
Type type,
GameObject go)
@ -145,6 +149,8 @@ namespace UnityExplorer.Tests
ExplorerCore.Log("Finished TestClass Init_Mono");
}
#endregion
#if CPP
public static Il2CppSystem.Collections.Generic.Dictionary<string, string> IL2CPP_Dict;
public static Il2CppSystem.Collections.Generic.HashSet<string> IL2CPP_HashSet;
@ -156,7 +162,7 @@ namespace UnityExplorer.Tests
public static Il2CppSystem.Collections.IDictionary IL2CPP_IDict;
public static Il2CppSystem.Collections.IList IL2CPP_IList;
public static Dictionary<Il2CppSystem.Object, Il2CppSystem.Object> IL2CPP_BoxedDict;
public static Il2CppSystem.Object IL2CPP_BoxedInt;
public static Il2CppSystem.Int32 IL2CPP_Int;
public static Il2CppSystem.Decimal IL2CPP_Decimal;
@ -185,31 +191,31 @@ namespace UnityExplorer.Tests
IL2CPP_HashTable.Add("key1", "value1");
IL2CPP_HashTable.Add("key2", "value2");
IL2CPP_HashTable.Add("key3", "value3");
ExplorerCore.Log($"IL2CPP 3: Il2Cpp IDictionary");
var dict2 = new Il2CppSystem.Collections.Generic.Dictionary<string, string>();
dict2.Add("key1", "value1");
IL2CPP_IDict = dict2.TryCast<Il2CppSystem.Collections.IDictionary>();
ExplorerCore.Log($"IL2CPP 4: Il2Cpp List of Il2Cpp Object");
var list = new Il2CppSystem.Collections.Generic.List<Il2CppSystem.Object>(5);
list.Add("one");
list.Add("two");
IL2CPP_IList = list.TryCast<Il2CppSystem.Collections.IList>();
ExplorerCore.Log($"IL2CPP 5: Il2Cpp List of strings");
IL2CPP_ListString = new Il2CppSystem.Collections.Generic.List<string>();
IL2CPP_ListString.Add("hello,");
IL2CPP_ListString.Add("world!");
ExplorerCore.Log($"IL2CPP 7: Dictionary of Il2Cpp String and Il2Cpp Object");
IL2CPP_BoxedDict = new();
IL2CPP_BoxedDict[(Il2CppSystem.String)"one"] = new Il2CppSystem.Int32 { m_value = 1 }.BoxIl2CppObject();
IL2CPP_BoxedDict[(Il2CppSystem.String)"two"] = new Il2CppSystem.Int32 { m_value = 2 }.BoxIl2CppObject();
IL2CPP_BoxedDict[(Il2CppSystem.String)"three"] = new Il2CppSystem.Int32 { m_value = 3 }.BoxIl2CppObject();
IL2CPP_BoxedDict[(Il2CppSystem.String)"four"] = new Il2CppSystem.Int32 { m_value = 4 }.BoxIl2CppObject();
ExplorerCore.Log($"IL2CPP 8: List of boxed Il2Cpp Objects");
IL2CPP_listOfBoxedObjects = new List<Il2CppSystem.Object>();
IL2CPP_listOfBoxedObjects.Add((Il2CppSystem.String)"boxedString");
@ -224,16 +230,16 @@ namespace UnityExplorer.Tests
var boxedEnum = Il2CppSystem.Enum.Parse(cppType, "Color");
IL2CPP_listOfBoxedObjects.Add(boxedEnum);
}
var structBox = Vector3.one.BoxIl2CppObject();
IL2CPP_listOfBoxedObjects.Add(structBox);
}
catch (Exception ex)
{
ExplorerCore.LogWarning($"Boxed enum test fail: {ex}");
}
ExplorerCore.Log($"IL2CPP 9: Il2Cpp struct array of ints");
IL2CPP_structArray = new UnhollowerBaseLib.Il2CppStructArray<int>(5);
IL2CPP_structArray[0] = 0;
@ -241,13 +247,13 @@ namespace UnityExplorer.Tests
IL2CPP_structArray[2] = 2;
IL2CPP_structArray[3] = 3;
IL2CPP_structArray[4] = 4;
ExplorerCore.Log($"IL2CPP 10: Il2Cpp reference array of boxed objects");
IL2CPP_ReferenceArray = new UnhollowerBaseLib.Il2CppReferenceArray<Il2CppSystem.Object>(3);
IL2CPP_ReferenceArray[0] = new Il2CppSystem.Int32 { m_value = 5 }.BoxIl2CppObject();
IL2CPP_ReferenceArray[1] = null;
IL2CPP_ReferenceArray[2] = (Il2CppSystem.String)"whats up";
ExplorerCore.Log($"IL2CPP 11: Misc il2cpp members");
IL2CPP_BoxedInt = new Il2CppSystem.Int32() { m_value = 5 }.BoxIl2CppObject();
IL2CPP_Int = new Il2CppSystem.Int32 { m_value = 420 };
@ -257,6 +263,7 @@ namespace UnityExplorer.Tests
ExplorerCore.Log($"Finished Init_Il2Cpp");
}
#endif
}
}

View File

@ -18,7 +18,10 @@ namespace UnityExplorer.UI
public static int Width => ActiveDisplay.renderingWidth;
public static int Height => ActiveDisplay.renderingHeight;
public static Vector3 MousePosition => Display.RelativeMouseAt(InputManager.MousePosition);
public static Vector3 MousePosition => Application.isEditor
? InputManager.MousePosition
: Display.RelativeMouseAt(InputManager.MousePosition);
public static bool MouseInTargetDisplay => MousePosition.z == ActiveDisplayIndex;
private static Camera canvasCamera;

View File

@ -30,13 +30,13 @@ namespace UnityExplorer.UI.Widgets.AutoComplete
public override int MinWidth => -1;
public override int MinHeight => -1;
public override bool CanDragAndResize => false;
public override bool CanDragAndResize => true;
public override bool ShouldSaveActiveState => false;
public override bool NavButtonWanted => false;
public static ISuggestionProvider CurrentHandler { get; private set; }
public static ButtonListHandler<Suggestion, ButtonCell> dataHandler;
public static ButtonListHandler<Suggestion, ButtonCell> buttonListDataHandler;
public static ScrollPool<ButtonCell> scrollPool;
private static GameObject navigationTipRow;
@ -82,7 +82,7 @@ namespace UnityExplorer.UI.Widgets.AutoComplete
{
base.UIRoot.SetActive(true);
base.UIRoot.transform.SetAsLastSibling();
dataHandler.RefreshData();
buttonListDataHandler.RefreshData();
scrollPool.Refresh(true, true);
}
}
@ -294,20 +294,40 @@ namespace UnityExplorer.UI.Widgets.AutoComplete
// UI Construction
const float MIN_X = 0.42f;
const float MAX_Y = 0.6f;
protected internal override void DoSetDefaultPosAndAnchors()
{
Rect.pivot = new Vector2(0f, 1f);
Rect.anchorMin = new Vector2(0.42f, 0.4f);
Rect.anchorMax = new Vector2(0.68f, 0.6f);
Rect.anchorMin = new Vector2(MIN_X, 0.4f);
Rect.anchorMax = new Vector2(0.68f, MAX_Y);
}
public override void OnFinishResize(RectTransform panel)
{
float xDiff = panel.anchorMin.x - MIN_X;
float yDiff = panel.anchorMax.y - MAX_Y;
if (xDiff != 0 || yDiff != 0)
{
panel.anchorMin = new(MIN_X, panel.anchorMin.y - yDiff);
panel.anchorMax = new(panel.anchorMax.x - xDiff, MAX_Y);
}
base.OnFinishResize(panel);
}
public override void ConstructPanelContent()
{
dataHandler = new ButtonListHandler<Suggestion, ButtonCell>(scrollPool, GetEntries, SetCell, ShouldDisplay, OnCellClicked);
// hide the titlebar
this.TitleBar.gameObject.SetActive(false);
buttonListDataHandler = new ButtonListHandler<Suggestion, ButtonCell>(scrollPool, GetEntries, SetCell, ShouldDisplay, OnCellClicked);
scrollPool = UIFactory.CreateScrollPool<ButtonCell>(this.uiContent, "AutoCompleter", out GameObject scrollObj,
out GameObject scrollContent);
scrollPool.Initialize(dataHandler);
scrollPool.Initialize(buttonListDataHandler);
UIFactory.SetLayoutElement(scrollObj, flexibleHeight: 9999);
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(scrollContent, true, false, true, false);

View File

@ -471,6 +471,9 @@ namespace UnityExplorer.UI.Panels
{
var text = UIFactory.CreateLabel(UIManager.UIRoot, "ResizeCursor", "↔", TextAnchor.MiddleCenter, Color.white, true, 35);
resizeCursorObj = text.gameObject;
var outline = text.gameObject.AddComponent<Outline>();
outline.effectColor = Color.black;
outline.effectDistance = new(1, 1);
RectTransform rect = resizeCursorObj.GetComponent<RectTransform>();
rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 64);

View File

@ -88,7 +88,7 @@ namespace UnityExplorer.UI
DisplayManager.Init();
var display = DisplayManager.ActiveDisplay;
Display display = DisplayManager.ActiveDisplay;
lastScreenWidth = display.renderingWidth;
lastScreenHeight = display.renderingHeight;

View File

@ -1,6 +1,7 @@
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.Linq;
using UniverseLib;
using UniverseLib.UI;
using UniverseLib.UI.Models;
@ -67,7 +68,17 @@ namespace UnityExplorer.UI.Widgets.AutoComplete
{
allowedTypes = new();
foreach (var entry in ReflectionUtility.AllTypes)
allowedTypes.Add(entry.Value);
{
// skip <PrivateImplementationDetails> and <AnonymousClass> classes
var type = entry.Value;
if (type.FullName.Contains("PrivateImplementationDetails")
|| type.FullName.Contains("DisplayClass")
|| type.FullName.Contains('<'))
{
continue;
}
allowedTypes.Add(type);
}
}
}

View File

@ -10,7 +10,7 @@ namespace UnityExplorer.UI.Widgets
{
public TransformTree Tree { get; }
public Transform Value { get; private set; }
public int InstanceID { get; private set; }
public int InstanceID { get; }
public CachedTransform Parent { get; internal set; }
public int Depth { get; internal set; }
@ -23,10 +23,11 @@ namespace UnityExplorer.UI.Widgets
public CachedTransform(TransformTree tree, Transform transform, int depth, CachedTransform parent = null)
{
InstanceID = transform.GetInstanceID();
Tree = tree;
Value = transform;
Parent = parent;
InstanceID = transform.GetInstanceID();
SiblingIndex = transform.GetSiblingIndex();
Update(transform, depth);
}

View File

@ -29,7 +29,6 @@ namespace UnityExplorer.UI.Widgets
public Action<GameObject> OnGameObjectClicked;
public CachedTransform cachedTransform;
public int cellIndex;
public GameObject UIRoot { get; set; }
public RectTransform Rect { get; set; }
@ -53,7 +52,7 @@ namespace UnityExplorer.UI.Widgets
UIRoot.SetActive(false);
}
public void ConfigureCell(CachedTransform cached, int cellIndex)
public void ConfigureCell(CachedTransform cached)
{
if (cached == null)
{
@ -64,7 +63,6 @@ namespace UnityExplorer.UI.Widgets
if (!Enabled)
Enable();
this.cellIndex = cellIndex;
cachedTransform = cached;
spacer.minWidth = cached.Depth * 15;
@ -153,7 +151,7 @@ namespace UnityExplorer.UI.Widgets
Rect.sizeDelta = new Vector2(25, 25);
UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0);
var spacerObj = UIFactory.CreateUIObject("Spacer", UIRoot, new Vector2(0, 0));
GameObject spacerObj = UIFactory.CreateUIObject("Spacer", UIRoot, new Vector2(0, 0));
UIFactory.SetLayoutElement(spacerObj, minWidth: 0, flexibleWidth: 0, minHeight: 0, flexibleHeight: 0);
this.spacer = spacerObj.GetComponent<LayoutElement>();
@ -164,15 +162,20 @@ namespace UnityExplorer.UI.Widgets
// Enabled toggle
var toggleObj = UIFactory.CreateToggle(UIRoot, "BehaviourToggle", out EnabledToggle, out var behavText, default, 17, 17);
GameObject toggleObj = UIFactory.CreateToggle(UIRoot, "BehaviourToggle", out EnabledToggle, out Text behavText, default, 17, 17);
UIFactory.SetLayoutElement(toggleObj, minHeight: 17, flexibleHeight: 0, minWidth: 17);
EnabledToggle.onValueChanged.AddListener(OnEnableClicked);
// Name button
NameButton = UIFactory.CreateButton(this.UIRoot, "NameButton", "Name", null);
GameObject nameBtnHolder = UIFactory.CreateHorizontalGroup(this.UIRoot, "NameButtonHolder",
false, false, true, true, childAlignment: TextAnchor.MiddleLeft);
UIFactory.SetLayoutElement(nameBtnHolder, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0);
nameBtnHolder.AddComponent<Mask>().showMaskGraphic = false;
NameButton = UIFactory.CreateButton(nameBtnHolder, "NameButton", "Name", null);
UIFactory.SetLayoutElement(NameButton.Component.gameObject, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0);
var nameLabel = NameButton.Component.GetComponentInChildren<Text>();
Text nameLabel = NameButton.Component.GetComponentInChildren<Text>();
nameLabel.horizontalOverflow = HorizontalWrapMode.Overflow;
nameLabel.alignment = TextAnchor.MiddleLeft;
@ -181,7 +184,7 @@ namespace UnityExplorer.UI.Widgets
SiblingIndex = UIFactory.CreateInputField(this.UIRoot, "SiblingIndexInput", string.Empty);
SiblingIndex.Component.textComponent.fontSize = 11;
SiblingIndex.Component.textComponent.alignment = TextAnchor.MiddleRight;
var siblingImage = SiblingIndex.GameObject.GetComponent<Image>();
Image siblingImage = SiblingIndex.GameObject.GetComponent<Image>();
siblingImage.color = new(0f, 0f, 0f, 0.25f);
UIFactory.SetLayoutElement(SiblingIndex.GameObject, 35, 20, 0, 0);
SiblingIndex.Component.GetOnEndEdit().AddListener(OnSiblingIndexEndEdit);

View File

@ -22,8 +22,8 @@ namespace UnityExplorer.UI.Widgets
// - Remove(object)
// - set_Item[object]
// These two methods have extremely bad performance due to using IndexOfKey(), which iterates the whole dictionary.
// Currently we do not use either of these methods, so everything should be constant time hash lookups.
// We DO make use of get_Item[object], get_Item[index], Add, Insert and RemoveAt, which OrderedDictionary perfectly meets our needs for.
// Currently we do not use either of these methods, so everything should be constant time lookups.
// We DO make use of get_Item[object], get_Item[index], Add, Insert, Contains and RemoveAt, which OrderedDictionary meets our needs for.
/// <summary>
/// Key: UnityEngine.Transform instance ID<br/>
/// Value: CachedTransform
@ -182,25 +182,25 @@ namespace UnityExplorer.UI.Widgets
traversedThisFrame.Reset();
traversedThisFrame.Start();
IEnumerable<GameObject> rootObjects = GetRootEntriesMethod.Invoke();
refreshCoroutine = RuntimeHelper.StartCoroutine(RefreshCoroutine(rootObjects, andRefreshUI, jumpToTop, oneShot));
refreshCoroutine = RuntimeHelper.StartCoroutine(RefreshCoroutine(andRefreshUI, jumpToTop, oneShot));
}
// Coroutine for batched updates, max 2000 gameobjects per frame so FPS doesn't get tanked when there is like 100k gameobjects.
// if "oneShot", then this will NOT be batched (if we need an immediate full update).
IEnumerator RefreshCoroutine(IEnumerable<GameObject> rootObjects, bool andRefreshUI, bool jumpToTop, bool oneShot)
IEnumerator RefreshCoroutine(bool andRefreshUI, bool jumpToTop, bool oneShot)
{
// Instead of doing string.IsNullOrEmpty(CurrentFilter) many times, let's just do it once per update.
bool filtering = Filtering;
IEnumerable<GameObject> rootObjects = GetRootEntriesMethod();
foreach (var gameObj in rootObjects)
{
if (gameObj)
if (!gameObj)
continue;
IEnumerator enumerator = Traverse(gameObj.transform, null, 0, oneShot, filtering);
while (enumerator.MoveNext())
{
var enumerator = Traverse(gameObj.transform, null, 0, oneShot);
while (enumerator.MoveNext())
{
if (!oneShot)
yield return enumerator.Current;
}
if (!oneShot)
yield return enumerator.Current;
}
}
@ -224,7 +224,7 @@ namespace UnityExplorer.UI.Widgets
// Recursive method to check a Transform and its children (if expanded).
// Parent and depth can be null/default.
private IEnumerator Traverse(Transform transform, CachedTransform parent, int depth, bool oneShot)
private IEnumerator Traverse(Transform transform, CachedTransform parent, int depth, bool oneShot, bool filtering)
{
// Let's only tank 2ms of each frame (60->53fps)
if (traversedThisFrame.ElapsedMilliseconds > 2)
@ -236,21 +236,20 @@ namespace UnityExplorer.UI.Widgets
int instanceID = transform.GetInstanceID();
// Unlikely, but since this method is async it could theoretically happen in extremely rare circumstances
if (visited.Contains(instanceID))
yield break;
if (Filtering)
if (filtering)
{
if (!FilterHierarchy(transform))
yield break;
visited.Add(instanceID);
if (!autoExpandedIDs.Contains(instanceID))
autoExpandedIDs.Add(instanceID);
}
else
visited.Add(instanceID);
visited.Add(instanceID);
CachedTransform cached;
if (cachedTransforms.Contains(instanceID))
@ -286,9 +285,11 @@ namespace UnityExplorer.UI.Widgets
if (IsTransformExpanded(instanceID) && cached.Value.childCount > 0)
{
ExplorerCore.Log($"Traversing expanded transform {cached.Value.name} ({cached.InstanceID})");
for (int i = 0; i < transform.childCount; i++)
{
var enumerator = Traverse(transform.GetChild(i), cached, depth + 1, oneShot);
var enumerator = Traverse(transform.GetChild(i), cached, depth + 1, oneShot, filtering);
while (enumerator.MoveNext())
{
if (!oneShot)
@ -317,7 +318,7 @@ namespace UnityExplorer.UI.Widgets
{
if (index < cachedTransforms.Count)
{
cell.ConfigureCell((CachedTransform)cachedTransforms[index], index);
cell.ConfigureCell((CachedTransform)cachedTransforms[index]);
if (Filtering)
{
if (cell.cachedTransform.Name.ContainsIgnoreCase(currentFilter))
@ -345,6 +346,8 @@ namespace UnityExplorer.UI.Widgets
public void OnCellExpandToggled(CachedTransform cache)
{
ExplorerCore.Log($"OnCellExpandToggled: {cache.Value.name} ({cache.InstanceID})");
var instanceID = cache.InstanceID;
if (expandedInstanceIDs.Contains(instanceID))
expandedInstanceIDs.Remove(instanceID);

View File

@ -0,0 +1,409 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityExplorer.Config;
using UnityExplorer.Inspectors;
using UniverseLib;
using UniverseLib.UI;
using UniverseLib.UI.Models;
using UniverseLib.UI.ObjectPool;
using UniverseLib.Utility;
namespace UnityExplorer.UI.Widgets
{
public class AudioClipWidget : UnityObjectWidget
{
static GameObject AudioPlayerObject;
static AudioSource Source;
static AudioClipWidget CurrentlyPlaying;
static Coroutine CurrentlyPlayingCoroutine;
static readonly string zeroLengthString = GetLengthString(0f);
public AudioClip RefAudioClip;
private string fullLengthText;
private ButtonRef toggleButton;
private bool audioPlayerWanted;
private GameObject audioPlayerRoot;
private ButtonRef playStopButton;
private Text progressLabel;
private GameObject saveObjectRow;
private InputFieldRef savePathInput;
private GameObject cantSaveRow;
public override void OnBorrowed(object target, Type targetType, ReflectionInspector inspector)
{
base.OnBorrowed(target, targetType, inspector);
this.audioPlayerRoot.transform.SetParent(inspector.UIRoot.transform);
this.audioPlayerRoot.transform.SetSiblingIndex(inspector.UIRoot.transform.childCount - 2);
RefAudioClip = target.TryCast<AudioClip>();
this.fullLengthText = GetLengthString(RefAudioClip.length);
if (RefAudioClip.loadType == AudioClipLoadType.DecompressOnLoad)
{
cantSaveRow.SetActive(false);
saveObjectRow.SetActive(true);
SetDefaultSavePath();
}
else
{
cantSaveRow.SetActive(true);
saveObjectRow.SetActive(false);
}
ResetProgressLabel();
}
public override void OnReturnToPool()
{
RefAudioClip = null;
if (audioPlayerWanted)
ToggleAudioWidget();
if (CurrentlyPlaying == this)
StopClip();
this.audioPlayerRoot.transform.SetParent(Pool<AudioClipWidget>.Instance.InactiveHolder.transform);
base.OnReturnToPool();
}
private void ToggleAudioWidget()
{
if (audioPlayerWanted)
{
audioPlayerWanted = false;
toggleButton.ButtonText.text = "Show Player";
audioPlayerRoot.SetActive(false);
}
else
{
audioPlayerWanted = true;
toggleButton.ButtonText.text = "Hide Player";
audioPlayerRoot.SetActive(true);
}
}
void SetDefaultSavePath()
{
string name = RefAudioClip.name;
if (string.IsNullOrEmpty(name))
name = "untitled";
savePathInput.Text = Path.Combine(ConfigManager.Default_Output_Path.Value, $"{name}.wav");
}
static string GetLengthString(float seconds)
{
TimeSpan ts = TimeSpan.FromSeconds(seconds);
StringBuilder sb = new();
if (ts.Hours > 0)
sb.Append($"{ts.Hours}:");
sb.Append($"{ts.Minutes:00}:");
sb.Append($"{ts.Seconds:00}:");
sb.Append($"{ts.Milliseconds:000}");
return sb.ToString();
}
private void ResetProgressLabel()
{
this.progressLabel.text = $"{zeroLengthString} / {fullLengthText}";
}
private void OnPlayStopClicked()
{
SetupAudioPlayer();
if (CurrentlyPlaying == this)
{
// we are playing a clip. stop it.
StopClip();
}
else
{
// If something else is playing a clip, stop that.
if (CurrentlyPlaying != null)
CurrentlyPlaying.StopClip();
// we want to start playing a clip.
CurrentlyPlayingCoroutine = RuntimeHelper.StartCoroutine(PlayClipCoroutine());
}
}
static void SetupAudioPlayer()
{
if (AudioPlayerObject)
return;
AudioPlayerObject = new GameObject("UnityExplorer.AudioPlayer");
UnityEngine.Object.DontDestroyOnLoad(AudioPlayerObject);
AudioPlayerObject.hideFlags = HideFlags.HideAndDontSave;
AudioPlayerObject.transform.position = new(int.MinValue, int.MinValue); // move it as far away as possible
#if CPP
Source = AudioPlayerObject.AddComponent(UnhollowerRuntimeLib.Il2CppType.Of<AudioSource>()).TryCast<AudioSource>();
#else
Source = AudioPlayerObject.AddComponent<AudioSource>();
#endif
AudioPlayerObject.AddComponent<AudioListener>();
}
private IEnumerator PlayClipCoroutine()
{
playStopButton.ButtonText.text = "Stop Clip";
CurrentlyPlaying = this;
Source.clip = this.RefAudioClip;
Source.Play();
while (Source.isPlaying)
{
progressLabel.text = $"{GetLengthString(Source.time)} / {fullLengthText}";
yield return null;
}
CurrentlyPlayingCoroutine = null;
StopClip();
}
private void StopClip()
{
if (CurrentlyPlayingCoroutine != null)
RuntimeHelper.StopCoroutine(CurrentlyPlayingCoroutine);
Source.Stop();
CurrentlyPlaying = null;
CurrentlyPlayingCoroutine = null;
playStopButton.ButtonText.text = "Play Clip";
ResetProgressLabel();
}
public void OnSaveClipClicked()
{
if (!RefAudioClip)
{
ExplorerCore.LogWarning("AudioClip is null, maybe it was destroyed?");
return;
}
if (string.IsNullOrEmpty(savePathInput.Text))
{
ExplorerCore.LogWarning("Save path cannot be empty!");
return;
}
string path = savePathInput.Text;
if (!path.EndsWith(".wav", StringComparison.InvariantCultureIgnoreCase))
path += ".wav";
path = IOUtility.EnsureValidFilePath(path);
if (File.Exists(path))
File.Delete(path);
SavWav.Save(RefAudioClip, path);
}
public override GameObject CreateContent(GameObject uiRoot)
{
GameObject ret = base.CreateContent(uiRoot);
// Toggle Button
toggleButton = UIFactory.CreateButton(UIRoot, "AudioWidgetToggleButton", "Show Player", new Color(0.2f, 0.3f, 0.2f));
toggleButton.Transform.SetSiblingIndex(0);
UIFactory.SetLayoutElement(toggleButton.Component.gameObject, minHeight: 25, minWidth: 170);
toggleButton.OnClick += ToggleAudioWidget;
// Actual widget
audioPlayerRoot = UIFactory.CreateVerticalGroup(uiRoot, "AudioWidget", false, false, true, true, spacing: 5);
UIFactory.SetLayoutElement(audioPlayerRoot, flexibleWidth: 9999, flexibleHeight: 50);
audioPlayerRoot.SetActive(false);
// Player
GameObject playerRow = UIFactory.CreateHorizontalGroup(audioPlayerRoot, "PlayerWidget", false, false, true, true,
spacing: 5, padding: new() { x = 3f, w = 3f, y = 3f, z = 3f });
playStopButton = UIFactory.CreateButton(playerRow, "PlayerButton", "Play", normalColor: new(0.2f, 0.4f, 0.2f));
playStopButton.OnClick += OnPlayStopClicked;
UIFactory.SetLayoutElement(playStopButton.GameObject, minWidth: 60, minHeight: 25);
progressLabel = UIFactory.CreateLabel(playerRow, "ProgressLabel", "0 / 0");
UIFactory.SetLayoutElement(progressLabel.gameObject, flexibleWidth: 9999, minHeight: 25);
ResetProgressLabel();
// Save helper
saveObjectRow = UIFactory.CreateHorizontalGroup(audioPlayerRoot, "SaveRow", false, false, true, true, 2, new Vector4(2, 2, 2, 2),
new Color(0.1f, 0.1f, 0.1f));
ButtonRef saveBtn = UIFactory.CreateButton(saveObjectRow, "SaveButton", "Save .WAV", new Color(0.2f, 0.25f, 0.2f));
UIFactory.SetLayoutElement(saveBtn.Component.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0);
saveBtn.OnClick += OnSaveClipClicked;
savePathInput = UIFactory.CreateInputField(saveObjectRow, "SaveInput", "...");
UIFactory.SetLayoutElement(savePathInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 9999);
// cant save label
cantSaveRow = UIFactory.CreateHorizontalGroup(audioPlayerRoot, "CantSaveRow", true, true, true, true);
UIFactory.SetLayoutElement(cantSaveRow, minHeight: 25, flexibleWidth: 9999);
UIFactory.CreateLabel(
cantSaveRow,
"CantSaveLabel",
"Cannot save this AudioClip as the data is compressed or streamed. Try a tool such as AssetRipper to unpack it.",
color: Color.grey);
return ret;
}
}
#region SavWav
// Copyright (c) 2012 Calvin Rien
// http://the.darktable.com
//
// This software is provided 'as-is', without any express or implied warranty. In
// no event will the authors be held liable for any damages arising from the use
// of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it freely,
// subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not claim
// that you wrote the original software. If you use this software in a product,
// an acknowledgment in the product documentation would be appreciated but is not
// required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
// =============================================================================
//
// derived from Gregorio Zanon's script
// http://forum.unity3d.com/threads/119295-Writing-AudioListener.GetOutputData-to-wav-problem?p=806734&viewfull=1#post806734
public static class SavWav
{
public const int HEADER_SIZE = 44;
public const float RESCALE_FACTOR = 32767; // to convert float to Int16
public static void Save(AudioClip clip, string filepath)
{
using FileStream fileStream = CreateEmpty(filepath);
ConvertAndWrite(fileStream, clip);
WriteHeader(fileStream, clip);
}
static FileStream CreateEmpty(string filepath)
{
FileStream fileStream = new(filepath, FileMode.Create);
byte emptyByte = default;
for (int i = 0; i < HEADER_SIZE; i++) //preparing the header
fileStream.WriteByte(emptyByte);
return fileStream;
}
static void ConvertAndWrite(FileStream fileStream, AudioClip clip)
{
#if CPP
UnhollowerBaseLib.Il2CppStructArray<float> samples = new float[clip.samples * clip.channels];
AudioClip.GetData(clip, samples, clip.samples, 0);
#else
float[] samples = new float[clip.samples * clip.channels];
clip.GetData(samples, 0);
#endif
int len = samples.Length;
// converting in 2 float[] steps to Int16[], then Int16[] to Byte[]
short[] intData = new short[len];
// bytesData array is twice the size of dataSource array because a float converted in Int16 is 2 bytes.
byte[] bytesData = new byte[len * 2];
for (int i = 0; i < len; i++)
{
intData[i] = (short)(samples[i] * RESCALE_FACTOR);
byte[] byteArr = BitConverter.GetBytes(intData[i]);
byteArr.CopyTo(bytesData, i * 2);
}
fileStream.Write(bytesData, 0, bytesData.Length);
}
static void WriteHeader(FileStream stream, AudioClip clip)
{
int hz = clip.frequency;
int channels = clip.channels;
int samples = clip.samples;
stream.Seek(0, SeekOrigin.Begin);
byte[] riff = Encoding.UTF8.GetBytes("RIFF");
stream.Write(riff, 0, 4);
byte[] chunkSize = BitConverter.GetBytes(stream.Length - 8);
stream.Write(chunkSize, 0, 4);
byte[] wave = Encoding.ASCII.GetBytes("WAVE");
stream.Write(wave, 0, 4);
byte[] fmt = Encoding.ASCII.GetBytes("fmt ");
stream.Write(fmt, 0, 4);
byte[] subChunk1 = BitConverter.GetBytes(16);
stream.Write(subChunk1, 0, 4);
byte[] audioFormat = BitConverter.GetBytes(1);
stream.Write(audioFormat, 0, 2);
byte[] numChannels = BitConverter.GetBytes(channels);
stream.Write(numChannels, 0, 2);
byte[] sampleRate = BitConverter.GetBytes(hz);
stream.Write(sampleRate, 0, 4);
byte[] byteRate = BitConverter.GetBytes(hz * channels * 2); // sampleRate * bytesPerSample*number of channels, here 44100*2*2
stream.Write(byteRate, 0, 4);
ushort blockAlign = (ushort)(channels * 2);
stream.Write(BitConverter.GetBytes(blockAlign), 0, 2);
ushort bps = 16;
byte[] bitsPerSample = BitConverter.GetBytes(bps);
stream.Write(bitsPerSample, 0, 2);
byte[] datastring = Encoding.UTF8.GetBytes("data");
stream.Write(datastring, 0, 4);
byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2);
stream.Write(subChunk2, 0, 4);
stream.Seek(0, SeekOrigin.Begin);
}
#endregion
}
}

View File

@ -0,0 +1,244 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityExplorer.Config;
using UnityExplorer.Inspectors;
using UnityExplorer.UI.Panels;
using UniverseLib;
using UniverseLib.Runtime;
using UniverseLib.UI;
using UniverseLib.UI.Models;
using UniverseLib.UI.ObjectPool;
using UniverseLib.Utility;
namespace UnityExplorer.UI.Widgets
{
public class Texture2DWidget : UnityObjectWidget
{
private Texture2D TextureRef;
private float realWidth;
private float realHeight;
private bool textureViewerWanted;
private ButtonRef toggleButton;
private GameObject textureViewerRoot;
private InputFieldRef savePathInput;
private Image image;
private LayoutElement imageLayout;
public override void OnBorrowed(object target, Type targetType, ReflectionInspector inspector)
{
base.OnBorrowed(target, targetType, inspector);
TextureRef = target.TryCast<Texture2D>();
realWidth = TextureRef.width;
realHeight = TextureRef.height;
if (this.textureViewerRoot)
this.textureViewerRoot.transform.SetParent(inspector.UIRoot.transform);
InspectorPanel.Instance.Dragger.OnFinishResize += OnInspectorFinishResize;
}
public override void OnReturnToPool()
{
InspectorPanel.Instance.Dragger.OnFinishResize -= OnInspectorFinishResize;
TextureRef = null;
if (image.sprite)
GameObject.Destroy(image.sprite);
if (textureViewerWanted)
ToggleTextureViewer();
if (this.textureViewerRoot)
this.textureViewerRoot.transform.SetParent(Pool<Texture2DWidget>.Instance.InactiveHolder.transform);
base.OnReturnToPool();
}
private void ToggleTextureViewer()
{
if (textureViewerWanted)
{
// disable
textureViewerWanted = false;
textureViewerRoot.SetActive(false);
toggleButton.ButtonText.text = "View Texture";
ParentInspector.mainContentHolder.SetActive(true);
}
else
{
// enable
if (!image.sprite)
SetupTextureViewer();
SetImageSize();
textureViewerWanted = true;
textureViewerRoot.SetActive(true);
toggleButton.ButtonText.text = "Hide Texture";
ParentInspector.mainContentHolder.gameObject.SetActive(false);
}
}
private void SetupTextureViewer()
{
if (!this.TextureRef)
return;
string name = TextureRef.name;
if (string.IsNullOrEmpty(name))
name = "untitled";
savePathInput.Text = Path.Combine(ConfigManager.Default_Output_Path.Value, $"{name}.png");
Sprite sprite = TextureHelper.CreateSprite(TextureRef);
image.sprite = sprite;
}
private void OnInspectorFinishResize(RectTransform _)
{
SetImageSize();
}
private void SetImageSize()
{
if (!imageLayout)
return;
RuntimeHelper.StartCoroutine(SetImageSizeCoro());
}
IEnumerator SetImageSizeCoro()
{
// let unity rebuild layout etc
yield return null;
RectTransform imageRect = InspectorPanel.Instance.Rect;
float rectWidth = imageRect.rect.width - 25;
float rectHeight = imageRect.rect.height - 196;
// If our image is smaller than the viewport, just use 100% scaling
if (realWidth < rectWidth && realHeight < rectHeight)
{
imageLayout.minWidth = realWidth;
imageLayout.minHeight = realHeight;
}
else // we will need to scale down the image to fit
{
// get the ratio of our viewport dimensions to width and height
float viewWidthRatio = (float)((decimal)rectWidth / (decimal)realWidth);
float viewHeightRatio = (float)((decimal)rectHeight / (decimal)realHeight);
// if width needs to be scaled more than height
if (viewWidthRatio < viewHeightRatio)
{
imageLayout.minWidth = realWidth * viewWidthRatio;
imageLayout.minHeight = realHeight * viewWidthRatio;
}
else // if height needs to be scaled more than width
{
imageLayout.minWidth = realWidth * viewHeightRatio;
imageLayout.minHeight = realHeight * viewHeightRatio;
}
}
}
private void OnSaveTextureClicked()
{
if (!TextureRef)
{
ExplorerCore.LogWarning("Texture is null, maybe it was destroyed?");
return;
}
if (string.IsNullOrEmpty(savePathInput.Text))
{
ExplorerCore.LogWarning("Save path cannot be empty!");
return;
}
string path = savePathInput.Text;
if (!path.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase))
path += ".png";
path = IOUtility.EnsureValidFilePath(path);
if (File.Exists(path))
File.Delete(path);
Texture2D tex = TextureRef;
if (!TextureHelper.IsReadable(tex))
tex = TextureHelper.ForceReadTexture(tex);
byte[] data = TextureHelper.EncodeToPNG(tex);
File.WriteAllBytes(path, data);
if (tex != TextureRef)
{
// cleanup temp texture if we had to force-read it.
GameObject.Destroy(tex);
}
}
public override GameObject CreateContent(GameObject uiRoot)
{
GameObject ret = base.CreateContent(uiRoot);
// Button
toggleButton = UIFactory.CreateButton(UIRoot, "TextureButton", "View Texture", new Color(0.2f, 0.3f, 0.2f));
toggleButton.Transform.SetSiblingIndex(0);
UIFactory.SetLayoutElement(toggleButton.Component.gameObject, minHeight: 25, minWidth: 150);
toggleButton.OnClick += ToggleTextureViewer;
// Texture viewer
textureViewerRoot = UIFactory.CreateVerticalGroup(uiRoot, "TextureViewer", false, false, true, true, 2, new Vector4(5, 5, 5, 5),
new Color(0.1f, 0.1f, 0.1f), childAlignment: TextAnchor.UpperLeft);
UIFactory.SetLayoutElement(textureViewerRoot, flexibleWidth: 9999, flexibleHeight: 9999);
// Save helper
GameObject saveRowObj = UIFactory.CreateHorizontalGroup(textureViewerRoot, "SaveRow", false, false, true, true, 2, new Vector4(2, 2, 2, 2),
new Color(0.1f, 0.1f, 0.1f));
ButtonRef saveBtn = UIFactory.CreateButton(saveRowObj, "SaveButton", "Save .PNG", new Color(0.2f, 0.25f, 0.2f));
UIFactory.SetLayoutElement(saveBtn.Component.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0);
saveBtn.OnClick += OnSaveTextureClicked;
savePathInput = UIFactory.CreateInputField(saveRowObj, "SaveInput", "...");
UIFactory.SetLayoutElement(savePathInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 9999);
// Actual texture viewer
GameObject imageViewport = UIFactory.CreateVerticalGroup(textureViewerRoot, "ImageViewport", false, false, true, true,
bgColor: new(1,1,1,0), childAlignment: TextAnchor.MiddleCenter);
UIFactory.SetLayoutElement(imageViewport, flexibleWidth: 9999, flexibleHeight: 9999);
GameObject imageHolder = UIFactory.CreateUIObject("ImageHolder", imageViewport);
imageLayout = UIFactory.SetLayoutElement(imageHolder, 1, 1, 0, 0);
var actualImageObj = UIFactory.CreateUIObject("ActualImage", imageHolder);
var actualRect = actualImageObj.GetComponent<RectTransform>();
actualRect.anchorMin = new(0, 0);
actualRect.anchorMax = new(1, 1);
image = actualImageObj.AddComponent<Image>();
textureViewerRoot.SetActive(false);
return ret;
}
}
}

View File

@ -0,0 +1,131 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityExplorer.Inspectors;
using UniverseLib;
using UniverseLib.UI;
using UniverseLib.UI.Models;
using UniverseLib.UI.ObjectPool;
namespace UnityExplorer.UI.Widgets
{
public class UnityObjectWidget : IPooledObject
{
public UnityEngine.Object UnityObjectRef;
public Component ComponentRef;
public ReflectionInspector ParentInspector;
protected ButtonRef gameObjectButton;
protected InputFieldRef nameInput;
protected InputFieldRef instanceIdInput;
// IPooledObject
public GameObject UIRoot { get; set; }
public float DefaultHeight => -1;
public static UnityObjectWidget GetUnityWidget(object target, Type targetType, ReflectionInspector inspector)
{
if (!typeof(UnityEngine.Object).IsAssignableFrom(targetType))
return null;
UnityObjectWidget widget = target switch
{
Texture2D => Pool<Texture2DWidget>.Borrow(),
AudioClip => Pool<AudioClipWidget>.Borrow(),
_ => Pool<UnityObjectWidget>.Borrow()
};
widget.OnBorrowed(target, targetType, inspector);
return widget;
}
public virtual void OnBorrowed(object target, Type targetType, ReflectionInspector inspector)
{
this.ParentInspector = inspector ?? throw new ArgumentNullException(nameof(inspector));
if (!this.UIRoot)
CreateContent(inspector.UIRoot);
else
this.UIRoot.transform.SetParent(inspector.UIRoot.transform);
this.UIRoot.transform.SetSiblingIndex(inspector.UIRoot.transform.childCount - 2);
UnityObjectRef = target.TryCast<UnityEngine.Object>();
UIRoot.SetActive(true);
nameInput.Text = UnityObjectRef.name;
instanceIdInput.Text = UnityObjectRef.GetInstanceID().ToString();
if (typeof(Component).IsAssignableFrom(targetType))
{
ComponentRef = (Component)target.TryCast(typeof(Component));
gameObjectButton.Component.gameObject.SetActive(true);
}
else
gameObjectButton.Component.gameObject.SetActive(false);
}
public virtual void OnReturnToPool()
{
UnityObjectRef = null;
ComponentRef = null;
ParentInspector = null;
}
// Update
public virtual void Update()
{
if (this.UnityObjectRef)
{
nameInput.Text = UnityObjectRef.name;
ParentInspector.Tab.TabText.text = $"{ParentInspector.currentBaseTabText} \"{UnityObjectRef.name}\"";
}
}
// UI Listeners
private void OnGameObjectButtonClicked()
{
if (!ComponentRef)
{
ExplorerCore.LogWarning("Component reference is null or destroyed!");
return;
}
InspectorManager.Inspect(ComponentRef.gameObject);
}
// UI construction
public virtual GameObject CreateContent(GameObject uiRoot)
{
UIRoot = UIFactory.CreateUIObject("UnityObjectRow", uiRoot);
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(UIRoot, false, false, true, true, 5);
UIFactory.SetLayoutElement(UIRoot, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999);
var nameLabel = UIFactory.CreateLabel(UIRoot, "NameLabel", "Name:", TextAnchor.MiddleLeft, Color.grey);
UIFactory.SetLayoutElement(nameLabel.gameObject, minHeight: 25, minWidth: 45, flexibleWidth: 0);
nameInput = UIFactory.CreateInputField(UIRoot, "NameInput", "untitled");
UIFactory.SetLayoutElement(nameInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 1000);
nameInput.Component.readOnly = true;
gameObjectButton = UIFactory.CreateButton(UIRoot, "GameObjectButton", "Inspect GameObject", new Color(0.2f, 0.2f, 0.2f));
UIFactory.SetLayoutElement(gameObjectButton.Component.gameObject, minHeight: 25, minWidth: 160);
gameObjectButton.OnClick += OnGameObjectButtonClicked;
var instanceLabel = UIFactory.CreateLabel(UIRoot, "InstanceLabel", "Instance ID:", TextAnchor.MiddleRight, Color.grey);
UIFactory.SetLayoutElement(instanceLabel.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0);
instanceIdInput = UIFactory.CreateInputField(UIRoot, "InstanceIDInput", "ERROR");
UIFactory.SetLayoutElement(instanceIdInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 0);
instanceIdInput.Component.readOnly = true;
UIRoot.SetActive(false);
return UIRoot;
}
}
}

View File

@ -175,13 +175,13 @@
<Private>False</Private>
</Reference>
<Reference Include="UniverseLib.Mono">
<HintPath>packages\UniverseLib.1.2.16\lib\net35\UniverseLib.Mono.dll</HintPath>
<HintPath>packages\UniverseLib.1.2.18\lib\net35\UniverseLib.Mono.dll</HintPath>
</Reference>
</ItemGroup>
<!-- Il2Cpp refs -->
<ItemGroup Condition="'$(IsCpp)'=='true'">
<Reference Include="UniverseLib.IL2CPP">
<HintPath>packages\UniverseLib.1.2.16\lib\net472\UniverseLib.IL2CPP.dll</HintPath>
<HintPath>packages\UniverseLib.1.2.18\lib\net472\UniverseLib.IL2CPP.dll</HintPath>
</Reference>
<Reference Include="UnhollowerBaseLib, Version=0.4.22.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>packages\Il2CppAssemblyUnhollower.BaseLib.0.4.22\lib\net472\UnhollowerBaseLib.dll</HintPath>
@ -199,6 +199,10 @@
<HintPath>..\lib\unhollowed\UnityEngine.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.AudioModule">
<HintPath>..\lib\unhollowed\UnityEngine.AudioModule.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\lib\unhollowed\UnityEngine.CoreModule.dll</HintPath>
<Private>False</Private>
@ -226,6 +230,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="CacheObject\CacheConstructor.cs" />
<Compile Include="CacheObject\CacheMemberFactory.cs" />
<Compile Include="Hooks\HookCell.cs" />
<Compile Include="Hooks\HookInstance.cs" />
<Compile Include="Hooks\HookManager.cs" />
@ -260,6 +265,8 @@
<Compile Include="CacheObject\Views\CacheListEntryCell.cs" />
<Compile Include="CacheObject\Views\CacheMemberCell.cs" />
<Compile Include="CacheObject\Views\CacheObjectCell.cs" />
<Compile Include="Loader\Standalone\Editor\ExplorerEditorBehaviour.cs" />
<Compile Include="Loader\Standalone\Editor\ExplorerEditorLoader.cs" />
<Compile Include="Runtime\UnityCrashPrevention.cs" />
<Compile Include="UI\DisplayManager.cs" />
<Compile Include="UI\Notification.cs" />
@ -313,7 +320,7 @@
<Compile Include="UI\Panels\ObjectExplorerPanel.cs" />
<Compile Include="UI\UIManager.cs" />
<Compile Include="UI\Panels\PanelDragger.cs" />
<Compile Include="UI\Widgets\AutoComplete\AutoCompleteModal.cs" />
<Compile Include="UI\Panels\AutoCompleteModal.cs" />
<Compile Include="UI\Widgets\AutoComplete\TypeCompleter.cs" />
<Compile Include="ObjectExplorer\ObjectSearch.cs" />
<Compile Include="ObjectExplorer\SceneExplorer.cs" />
@ -323,6 +330,9 @@
<Compile Include="UI\Widgets\TransformTree\CachedTransform.cs" />
<Compile Include="UI\Widgets\TransformTree\TransformCell.cs" />
<Compile Include="UI\Widgets\TransformTree\TransformTree.cs" />
<Compile Include="UI\Widgets\UnityObjects\AudioClipWidget.cs" />
<Compile Include="UI\Widgets\UnityObjects\Texture2DWidget.cs" />
<Compile Include="UI\Widgets\UnityObjects\UnityObjectWidget.cs" />
</ItemGroup>
<ItemGroup>
<None Include="ILRepack.targets" />

View File

@ -6,6 +6,6 @@
<package id="ILRepack.Lib.MSBuild.Task" version="2.0.18.2" targetFramework="net35" />
<package id="Mono.Cecil" version="0.10.4" targetFramework="net35" />
<package id="Samboy063.Tomlet" version="3.1.3" targetFramework="net472" />
<package id="UniverseLib" version="1.2.16" targetFramework="net35" />
<package id="UniverseLib" version="1.2.18" targetFramework="net35" />
<package id="UniverseLib.Analyzers" version="1.0.3" targetFramework="net35" developmentDependency="true" />
</packages>