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 componentsVisibilityByID = new(); private readonly Dictionary cachedFieldsByComponentID = new(); private readonly Dictionary cachedMethodsByComponentID = new(); private readonly Dictionary> 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) ? "" : 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 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()) { 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; } } } } } /// /// Draws an enum field on a component. /// /// The target containing the field. /// The enum field /// The selected enum value. 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 enumVisibilityByType)) { // Add an empty subdictionary, it will be handled by the statement below. enumVisibilityByType = new Dictionary(); 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()); 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(); } }