first commit
This commit is contained in:
483
NitroxClient/Debuggers/SceneDebugger.cs
Normal file
483
NitroxClient/Debuggers/SceneDebugger.cs
Normal file
@@ -0,0 +1,483 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using NitroxClient.Debuggers.Drawer;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace NitroxClient.Debuggers;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class SceneDebugger : BaseDebugger
|
||||
{
|
||||
private readonly DrawerManager drawerManager;
|
||||
public GameObject SelectedObject { get; private set; }
|
||||
private int selectedComponentID;
|
||||
private Scene selectedScene;
|
||||
|
||||
private bool showUnityMethods;
|
||||
private bool showSystemMethods;
|
||||
|
||||
private Vector2 gameObjectScrollPos;
|
||||
private Vector2 hierarchyScrollPos;
|
||||
|
||||
private readonly Dictionary<int, bool> componentsVisibilityByID = new();
|
||||
private readonly Dictionary<int, FieldInfo[]> cachedFieldsByComponentID = new();
|
||||
private readonly Dictionary<int, MethodInfo[]> cachedMethodsByComponentID = new();
|
||||
private readonly Dictionary<int, IDictionary<Type, bool>> enumVisibilityByComponentIDAndEnumType = new();
|
||||
|
||||
public SceneDebugger() : base(650, null, KeyCode.S, true, false, false, GUISkinCreationOptions.DERIVEDCOPY)
|
||||
{
|
||||
drawerManager = new DrawerManager(this);
|
||||
ActiveTab = AddTab("Scenes", RenderTabScenes);
|
||||
AddTab("Hierarchy", RenderTabHierarchy);
|
||||
AddTab("GameObject", RenderTabGameObject);
|
||||
}
|
||||
|
||||
protected override void OnSetSkin(GUISkin skin)
|
||||
{
|
||||
base.OnSetSkin(skin);
|
||||
|
||||
skin.SetCustomStyle("sceneLoaded", skin.label, s =>
|
||||
{
|
||||
s.normal = new GUIStyleState { textColor = Color.green };
|
||||
s.fontStyle = FontStyle.Bold;
|
||||
});
|
||||
|
||||
skin.SetCustomStyle("loadScene", skin.button, s => { s.fixedWidth = 60; });
|
||||
|
||||
skin.SetCustomStyle("fillMessage", skin.label, s =>
|
||||
{
|
||||
s.stretchWidth = true;
|
||||
s.stretchHeight = true;
|
||||
s.fontSize = 24;
|
||||
s.alignment = TextAnchor.MiddleCenter;
|
||||
s.fontStyle = FontStyle.Italic;
|
||||
});
|
||||
|
||||
skin.SetCustomStyle("breadcrumb", skin.label, s =>
|
||||
{
|
||||
s.fontSize = 20;
|
||||
s.fontStyle = FontStyle.Bold;
|
||||
});
|
||||
|
||||
skin.SetCustomStyle("breadcrumbNav", skin.box, s =>
|
||||
{
|
||||
s.stretchWidth = false;
|
||||
s.fixedWidth = 100;
|
||||
});
|
||||
|
||||
skin.SetCustomStyle("options", skin.textField, s =>
|
||||
{
|
||||
s.fixedWidth = 200;
|
||||
s.margin = new RectOffset(8, 8, 4, 4);
|
||||
});
|
||||
|
||||
skin.SetCustomStyle("options_label", skin.label, s => { s.alignment = TextAnchor.MiddleLeft; });
|
||||
|
||||
skin.SetCustomStyle("bold", skin.label, s =>
|
||||
{
|
||||
s.alignment = TextAnchor.LowerLeft;
|
||||
s.fontStyle = FontStyle.Bold;
|
||||
});
|
||||
|
||||
skin.SetCustomStyle("boxHighlighted", skin.box, s =>
|
||||
{
|
||||
Texture2D result = new(1, 1);
|
||||
result.SetPixels(new[] { new Color(1f, 0.9f, 0f, 0.25f) });
|
||||
result.Apply();
|
||||
s.normal.background = result;
|
||||
});
|
||||
}
|
||||
|
||||
private void RenderTabScenes()
|
||||
{
|
||||
using (new GUILayout.VerticalScope("box"))
|
||||
{
|
||||
GUILayout.Label("All scenes", "header");
|
||||
int maxSceneCount = Math.Max(SceneManager.sceneCount, SceneManager.sceneCountInBuildSettings);
|
||||
for (int i = 0; i < maxSceneCount + 1; i++)
|
||||
{
|
||||
// Getting the DontDestroyOnLoad though the NitroxBootstrapper instance
|
||||
Scene currentScene = i == maxSceneCount ? NitroxBootstrapper.Instance.gameObject.scene : SceneManager.GetSceneAt(i);
|
||||
|
||||
bool isSelected = selectedScene.IsValid() && currentScene == selectedScene;
|
||||
bool isLoaded = currentScene.isLoaded;
|
||||
bool isDDOLScene = currentScene.name == "DontDestroyOnLoad";
|
||||
|
||||
using (new GUILayout.HorizontalScope("box"))
|
||||
{
|
||||
if (GUILayout.Button($"{(isSelected ? ">> " : "")}{i}: {(isDDOLScene ? currentScene.name : currentScene.path.TruncateLeft(35))}", isLoaded ? "sceneLoaded" : "label"))
|
||||
{
|
||||
selectedScene = currentScene;
|
||||
ActiveTab = GetTab("Hierarchy").Value;
|
||||
}
|
||||
|
||||
if (isLoaded)
|
||||
{
|
||||
if (!isDDOLScene && GUILayout.Button("Unload", "loadScene"))
|
||||
{
|
||||
SceneManager.UnloadSceneAsync(i);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isDDOLScene && GUILayout.Button("Load", "loadScene"))
|
||||
{
|
||||
SceneManager.LoadSceneAsync(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderTabHierarchy()
|
||||
{
|
||||
using (new GUILayout.HorizontalScope("box"))
|
||||
{
|
||||
StringBuilder breadcrumbBuilder = new();
|
||||
if (SelectedObject)
|
||||
{
|
||||
Transform parent = SelectedObject.transform;
|
||||
while (parent)
|
||||
{
|
||||
breadcrumbBuilder.Insert(0, '/');
|
||||
breadcrumbBuilder.Insert(0, string.IsNullOrEmpty(parent.name) ? "<no-name>" : parent.name);
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
|
||||
breadcrumbBuilder.Insert(0, "//");
|
||||
GUILayout.Label(breadcrumbBuilder.ToString(), "breadcrumb");
|
||||
|
||||
using (new GUILayout.HorizontalScope("breadcrumbNav"))
|
||||
{
|
||||
if (GUILayout.Button("<<"))
|
||||
{
|
||||
UpdateSelectedObject(null);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("<") && SelectedObject && SelectedObject.transform.parent)
|
||||
{
|
||||
UpdateSelectedObject(SelectedObject.transform.parent.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (new GUILayout.VerticalScope("box"))
|
||||
{
|
||||
if (selectedScene.IsValid())
|
||||
{
|
||||
using GUILayout.ScrollViewScope scroll = new(hierarchyScrollPos);
|
||||
hierarchyScrollPos = scroll.scrollPosition;
|
||||
List<GameObject> showObjects = new();
|
||||
if (!SelectedObject)
|
||||
{
|
||||
showObjects = selectedScene.GetRootGameObjects().ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (Transform t in SelectedObject.transform)
|
||||
{
|
||||
showObjects.Add(t.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (GameObject child in showObjects)
|
||||
{
|
||||
if (GUILayout.Button(child.name, child.transform.childCount > 0 ? "bold" : "label"))
|
||||
{
|
||||
UpdateSelectedObject(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GUILayout.Label($"No selected scene\nClick on a Scene in '{GetTab("Hierarchy").Value.Name}'", "fillMessage");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderTabGameObject()
|
||||
{
|
||||
using (new GUILayout.VerticalScope("box"))
|
||||
{
|
||||
if (!SelectedObject)
|
||||
{
|
||||
GUILayout.Label($"No selected GameObject\nClick on an object in '{GetTab("Hierarchy").Value.Name}'", "fillMessage");
|
||||
return;
|
||||
}
|
||||
|
||||
using GUILayout.ScrollViewScope scroll = new(gameObjectScrollPos);
|
||||
gameObjectScrollPos = scroll.scrollPosition;
|
||||
|
||||
using (new GUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label($"GameObject: {SelectedObject.name}", "bold", GUILayout.Height(25));
|
||||
GUILayout.Space(5);
|
||||
}
|
||||
|
||||
foreach (Component component in SelectedObject.GetComponents<Component>())
|
||||
{
|
||||
int componentId = component.GetInstanceID();
|
||||
|
||||
using (new GUILayout.VerticalScope(selectedComponentID == componentId ? "boxHighlighted" : "box"))
|
||||
{
|
||||
if (!componentsVisibilityByID.TryGetValue(componentId, out bool visible))
|
||||
{
|
||||
visible = componentsVisibilityByID[componentId] = component is Transform or RectTransform; // Transform should be visible by default
|
||||
}
|
||||
|
||||
Type componentType = component.GetType();
|
||||
MonoBehaviour monoBehaviour = component as MonoBehaviour;
|
||||
|
||||
using (new GUILayout.HorizontalScope(GUILayout.Height(12f)))
|
||||
{
|
||||
if (monoBehaviour)
|
||||
{
|
||||
monoBehaviour.enabled = GUILayout.Toggle(monoBehaviour.enabled, GUIContent.none, GUILayout.Width(15));
|
||||
}
|
||||
|
||||
GUILayout.Label(componentType.Name, "bold", GUILayout.Height(20));
|
||||
NitroxGUILayout.Separator();
|
||||
|
||||
if (GUILayout.Button("Show / Hide", GUILayout.Width(100)))
|
||||
{
|
||||
componentsVisibilityByID[component.GetInstanceID()] = visible = !visible;
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.Space(5);
|
||||
|
||||
if (visible)
|
||||
{
|
||||
GUILayout.Space(10);
|
||||
if (!drawerManager.TryDraw(component))
|
||||
{
|
||||
if (monoBehaviour)
|
||||
{
|
||||
DrawFields(monoBehaviour);
|
||||
GUILayout.Space(20);
|
||||
DrawMonoBehaviourMethods(monoBehaviour);
|
||||
}
|
||||
else
|
||||
{
|
||||
NitroxGUILayout.Separator();
|
||||
GUILayout.Label("This component is not yet supported");
|
||||
GUILayout.Space(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.Space(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFields(UnityEngine.Object target)
|
||||
{
|
||||
if (!cachedFieldsByComponentID.TryGetValue(target.GetInstanceID(), out FieldInfo[] fields))
|
||||
{
|
||||
fields = cachedFieldsByComponentID[target.GetInstanceID()] = target.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
foreach (FieldInfo field in fields)
|
||||
{
|
||||
object fieldValue = field.GetValue(target);
|
||||
|
||||
using (new GUILayout.HorizontalScope("box", GUILayout.MinHeight(35)))
|
||||
{
|
||||
GUILayout.Label($"[{field.FieldType.ToString().Split('.').Last()}]: {field.Name}", "options_label");
|
||||
NitroxGUILayout.Separator();
|
||||
|
||||
if (fieldValue is Component component)
|
||||
{
|
||||
if (GUILayout.Button("Goto"))
|
||||
{
|
||||
JumpToComponent(component);
|
||||
}
|
||||
}
|
||||
else if (fieldValue != null && (field.FieldType.IsArray || typeof(IList).IsAssignableFrom(field.FieldType)))
|
||||
{
|
||||
IList list = (IList)field.GetValue(target);
|
||||
GUILayout.Box($"Length: {list.Count}", GUILayout.Width(NitroxGUILayout.VALUE_WIDTH));
|
||||
}
|
||||
else if (fieldValue != null && (typeof(IDictionary).IsAssignableFrom(field.FieldType)))
|
||||
{
|
||||
IDictionary dict = (IDictionary)field.GetValue(target);
|
||||
GUILayout.Box($"Length: {dict.Count}", GUILayout.Width(NitroxGUILayout.VALUE_WIDTH));
|
||||
}
|
||||
else if (drawerManager.TryDrawEditor(fieldValue, out object editedValue))
|
||||
{
|
||||
field.SetValue(target, editedValue);
|
||||
}
|
||||
else if (!drawerManager.TryDraw(fieldValue))
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
switch (fieldValue)
|
||||
{
|
||||
case null:
|
||||
GUILayout.Box("Field is null", GUILayout.Width(NitroxGUILayout.VALUE_WIDTH));
|
||||
break;
|
||||
case ScriptableObject scriptableObject:
|
||||
if (GUILayout.Button(field.Name, GUILayout.Width(NitroxGUILayout.VALUE_WIDTH)))
|
||||
{
|
||||
DrawFields(scriptableObject);
|
||||
}
|
||||
|
||||
break;
|
||||
case GameObject gameObject:
|
||||
if (GUILayout.Button(field.Name, GUILayout.Width(NitroxGUILayout.VALUE_WIDTH)))
|
||||
{
|
||||
UpdateSelectedObject(gameObject);
|
||||
}
|
||||
|
||||
break;
|
||||
case bool boolValue:
|
||||
if (GUILayout.Button(boolValue.ToString(), GUILayout.Width(NitroxGUILayout.VALUE_WIDTH)))
|
||||
{
|
||||
field.SetValue(target, !boolValue);
|
||||
}
|
||||
|
||||
break;
|
||||
case short:
|
||||
case ushort:
|
||||
case int:
|
||||
case uint:
|
||||
case long:
|
||||
case ulong:
|
||||
case float:
|
||||
case double:
|
||||
field.SetValue(target, NitroxGUILayout.ConvertibleField((IConvertible)fieldValue));
|
||||
break;
|
||||
case Enum enumValue:
|
||||
DrawEnum(target, field, enumValue);
|
||||
break;
|
||||
default:
|
||||
GUILayout.TextArea(fieldValue.ToString(), "options", GUILayout.Width(NitroxGUILayout.VALUE_WIDTH));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws an enum field on a component.
|
||||
/// </summary>
|
||||
/// <param name="target">The target containing the field.</param>
|
||||
/// <param name="field">The enum field</param>
|
||||
/// <param name="enumValue">The selected enum value.</param>
|
||||
private void DrawEnum(UnityEngine.Object target, FieldInfo field, Enum enumValue)
|
||||
{
|
||||
// This is the first time enountering this target type
|
||||
if (!enumVisibilityByComponentIDAndEnumType.TryGetValue(target.GetInstanceID(), out IDictionary<Type, bool> enumVisibilityByType))
|
||||
{
|
||||
// Add an empty subdictionary, it will be handled by the statement below.
|
||||
enumVisibilityByType = new Dictionary<Type, bool>();
|
||||
enumVisibilityByComponentIDAndEnumType.Add(target.GetInstanceID(), enumVisibilityByType);
|
||||
}
|
||||
|
||||
// This is the first time we are encountering this enum type on this component
|
||||
if (!enumVisibilityByType.TryGetValue(field.FieldType, out _))
|
||||
{
|
||||
enumVisibilityByType.Add(field.FieldType, false);
|
||||
}
|
||||
|
||||
using (new GUILayout.VerticalScope())
|
||||
{
|
||||
using (new GUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
// Toggle the visibility and save it in the subdictionary
|
||||
if (GUILayout.Button("Show / Hide", GUILayout.Width(NitroxGUILayout.VALUE_WIDTH)))
|
||||
{
|
||||
enumVisibilityByType[field.FieldType] = !enumVisibilityByType[field.FieldType];
|
||||
}
|
||||
}
|
||||
|
||||
// Show the enum list.
|
||||
if (enumVisibilityByType[field.FieldType])
|
||||
{
|
||||
GUILayout.Space(5);
|
||||
using (new GUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
field.SetValue(target, NitroxGUILayout.EnumPopup(enumValue, 250));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMonoBehaviourMethods(MonoBehaviour monoBehaviour)
|
||||
{
|
||||
using (new GUILayout.HorizontalScope("box"))
|
||||
{
|
||||
showSystemMethods = GUILayout.Toggle(showSystemMethods, "Show System inherit methods", GUILayout.Height(25));
|
||||
showUnityMethods = GUILayout.Toggle(showUnityMethods, "Show Unity inherit methods", GUILayout.Height(25));
|
||||
}
|
||||
|
||||
if (!cachedMethodsByComponentID.TryGetValue(monoBehaviour.GetInstanceID(), out MethodInfo[] methods))
|
||||
{
|
||||
methods = cachedMethodsByComponentID[monoBehaviour.GetInstanceID()] = monoBehaviour.GetType().GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy)
|
||||
.OrderBy(m => m.Name).ToArray();
|
||||
}
|
||||
|
||||
foreach (MethodInfo method in methods)
|
||||
{
|
||||
string methodAssemblyName = method.DeclaringType != null ? method.DeclaringType.Assembly.GetName().Name : string.Empty;
|
||||
if (!(!showSystemMethods && (methodAssemblyName.Contains("System") || methodAssemblyName.Contains("mscorlib"))) &&
|
||||
!(!showUnityMethods && methodAssemblyName.Contains("UnityEngine")))
|
||||
{
|
||||
using (new GUILayout.VerticalScope("box"))
|
||||
{
|
||||
GUILayout.Label(method.ToString());
|
||||
|
||||
if (method.GetParameters().Any()) // TODO: Allow methods with parameters to be called.
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Invoke", GUILayout.MaxWidth(150)))
|
||||
{
|
||||
object result = method.Invoke(method.IsStatic ? null : monoBehaviour, Array.Empty<object>());
|
||||
Log.InGame($"Invoked method {method.Name}");
|
||||
|
||||
if (method.ReturnType != typeof(void))
|
||||
{
|
||||
Log.InGame(result != null ? $"Returned: '{result}'" : "Return value was NULL.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateSelectedObject(GameObject item)
|
||||
{
|
||||
if (SelectedObject == item)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedObject = item;
|
||||
selectedComponentID = default;
|
||||
}
|
||||
|
||||
public void JumpToComponent(Component item)
|
||||
{
|
||||
UpdateSelectedObject(item.gameObject);
|
||||
RenderTabGameObject();
|
||||
selectedComponentID = item.GetInstanceID();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user