CORE dashboard + a lot of changes

This commit is contained in:
2026-06-22 20:09:15 +02:00
parent 19d6bd934a
commit 89fa0b23b2
101 changed files with 1525 additions and 177 deletions
@@ -0,0 +1,364 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using RPGCore.Core;
using RPGCore.Core.Objects;
using RPGCoreCommon.Helpers;
using RPGCoreCommon.Helpers.CustomTypes;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace RPGCore.Editor.CoreManager
{
internal class DashboardWindow : EditorWindow
{
[SerializeField] private VisualTreeAsset _visualTreeAsset;
// CONST
private const string IsFirstTimeOpenKey = "TheVVaS.RPGCore.isFirstTimeOpen";
private const string PipeStart = "├─";
private const string PipeGo = "│\u2007\u2007";
private const string PipeEnd = "└─";
private const string PipeNone = "\u2007\u2007\u2007";
// CACHE - check CreateCache()
private static Type[] _moduleTypes;
private static Type[] _objectTypes;
private static Dictionary<Type, List<Type>> _objectTypesMap;
private static Dictionary<Type, MonoScript> _moduleMonoScripts;
// TAB - VERIFY
private Button _verifyCheckButton;
private Button _verifyFixButton;
private Button _verifyClearButton;
private MultiColumnListView _verifyListMultiColumnListView;
private List<Validation> _validations;
// TAB - MODULES
private ToolbarSearchField _modulesFilterSearchFiled;
private ScrollView _modulesListScrollView;
// TAB - CONFIG
private PropertyField _configVerifyPathsProperty;
[InitializeOnLoadMethod]
private static void OnEditorLoad()
{
var isFirstTimeOpen = EditorPrefs.GetBool(IsFirstTimeOpenKey, true);
if (!isFirstTimeOpen) return;
ShowDashboard();
EditorPrefs.SetBool(IsFirstTimeOpenKey, false);
}
[MenuItem("TheVVaS/RPGCore/Dashboard", priority = -1)]
public static void ShowDashboard()
{
var window = GetWindow<DashboardWindow>();
window.titleContent = new GUIContent("RPGCore Dashboard");
window.Show();
}
private void CreateGUI()
{
CreateCache();
UpdateConfigModules();
_visualTreeAsset.CloneTree(rootVisualElement);
_verifyCheckButton = rootVisualElement.Q<Button>("db-verify-check");
_verifyCheckButton.clicked += OnVerifyCheck;
_verifyFixButton = rootVisualElement.Q<Button>("db-verify-fix");
_verifyFixButton.clicked += OnVerifyFix;
_verifyFixButton.SetEnabled(false);
_verifyClearButton = rootVisualElement.Q<Button>("db-verify-clear");
_verifyClearButton.clicked += OnVerifyClear;
_verifyListMultiColumnListView = rootVisualElement.Q<MultiColumnListView>("db-verify-list");
CreateVerifyList();
_modulesFilterSearchFiled = rootVisualElement.Q<ToolbarSearchField>("db-modules-filter");
_modulesFilterSearchFiled.RegisterValueChangedCallback(ev => OnModuleFilter(ev.newValue));
_modulesListScrollView = rootVisualElement.Q<ScrollView>("db-modules-list");
CreateModulesList();
_configVerifyPathsProperty = rootVisualElement.Q<PropertyField>("db-config-verify-paths");
_configVerifyPathsProperty.BindProperty(
new SerializedObject(EditorConfigSO.instance).FindProperty(nameof(EditorConfigSO.verifyPaths)));
}
private void CreateCache()
{
_moduleTypes = TypeCache.GetTypesDerivedFrom<ObjectModule>()
.OrderBy(t => t.Assembly.GetName().Name)
.Where(t => !t.IsAbstract && !t.IsInterface)
.ToArray();
_objectTypes = TypeCache.GetTypesDerivedFrom<BaseObject>()
.Append(typeof(BaseObject))
.Sort((t1, t2) => t1.IsAssignableFrom(t2) ? -1 : t2.IsAssignableFrom(t1) ? 1 : 0)
.ToArray();
_objectTypesMap = _objectTypes
.GroupBy(t => t.BaseType)
.Where(g => typeof(BaseObject).IsAssignableFrom(g.Key))
.ToDictionary(k => k.Key, v => v.ToList());
_moduleMonoScripts = MonoImporter.GetAllRuntimeMonoScripts()
.Where(m => typeof(ObjectModule).IsAssignableFrom(m.GetClass()))
.Where(m => !m.GetClass().IsAbstract && !m.GetClass().IsInterface)
.ToDictionary(m => m.GetClass(), m => m);
}
private void UpdateConfigModules()
{
var config = EditorConfigSO.instance;
foreach (var moduleType in _moduleTypes)
{
var isRequired = moduleType.GetCustomAttribute<ObjectModuleAttribute>()?.required ?? false;
var baseObjectType = moduleType.FindGenericDefinitionType(typeof(ObjectModule<>)).GetGenericArguments()[0];
config.modules.TryAdd(moduleType, new List<SerializableType>());
if (isRequired)
{
config.modules[moduleType] = new List<SerializableType> { baseObjectType };
}
else
{
config.modules[moduleType] = config.modules[moduleType]
.Where(t => !config.modules[moduleType].Any(st => t.type.IsSubclassOf(st)))
.ToList();
}
}
config.Save();
}
#region VERIFY
private void CreateVerifyList()
{
_verifyListMultiColumnListView.columns["validator"].makeCell = () => new Label();
_verifyListMultiColumnListView.columns["validator"].bindCell = (element, index) =>
{
((Label)element).text = element.tooltip = _validations[index].validator.GetType().Name;
};
_verifyListMultiColumnListView.columns["message"].makeCell = () => new Label();
_verifyListMultiColumnListView.columns["message"].bindCell = (element, index) =>
{
((Label)element).text = element.tooltip = _validations[index].message;
};
_verifyListMultiColumnListView.columns["context"].makeCell = () => new Label();
_verifyListMultiColumnListView.columns["context"].bindCell = (element, index) =>
{
((Label)element).text = element.tooltip = _validations[index].contextPath;
element.RegisterCallback<PointerDownEvent>(ev =>
{
if (ev.clickCount != 2) return;
PingObject(_validations[index].contextPath);
});
};
_verifyListMultiColumnListView.columns["hierarchy"].makeCell = () => new Label();
_verifyListMultiColumnListView.columns["hierarchy"].bindCell = (element, index) =>
{
((Label)element).text = element.tooltip = _validations[index].hierarchyPath;
element.RegisterCallback<PointerDownEvent>(ev =>
{
if (ev.clickCount != 2) return;
PingObject(_validations[index].contextPath, _validations[index].hierarchyPath);
});
};
_verifyListMultiColumnListView.columns["result"].makeCell = () => new Label();
_verifyListMultiColumnListView.columns["result"].bindCell = (element, index) =>
{
((Label)element).text = element.tooltip = _validations[index].resultMessage;
element.EnableInClassList("verify-success", _validations[index].result == true);
element.EnableInClassList("verify-error", _validations[index].result == false);
};
}
private void PingObject(string contextPath, string hierarchyPath = null)
{
var contextFile = AssetDatabase.LoadMainAssetAtPath(contextPath);
GameObject hierarchyObjectRoot;
if (hierarchyPath == null)
{
EditorGUIUtility.PingObject(contextFile.GetInstanceID());
return;
}
var hierarchyPathArray = hierarchyPath.Split('/', 2);
if (contextFile is SceneAsset)
{
hierarchyObjectRoot = EditorSceneManager.OpenScene(contextPath)
.GetRootGameObjects().FirstOrDefault(go => go.name == hierarchyPathArray[0]);
}
else if (contextFile is GameObject)
{
hierarchyObjectRoot = PrefabStageUtility.OpenPrefab(contextPath).prefabContentsRoot;
}
else
{
Debug.LogError($"Given file '{contextPath}' is not prefab nor scene!");
return;
}
if (!hierarchyObjectRoot)
{
Debug.LogError($"Can't find root game object '{hierarchyPathArray[0]}' in '{contextPath}'. Perhaps did you change something?");
return;
}
var hierarchyObject = hierarchyPathArray.Length > 1 ?
hierarchyObjectRoot.transform.Find(hierarchyPathArray[1])?.gameObject : hierarchyObjectRoot;
if (!hierarchyObject)
{
Debug.LogError($"Can't find '{hierarchyPath}' in '{contextPath}'. Perhaps did you change something?");
return;
}
EditorGUIUtility.PingObject(hierarchyObject.GetInstanceID());
}
private void OnVerifyCheck()
{
_validations = ValidationSolver.Check(EditorConfigSO.instance.verifyPaths);
_verifyListMultiColumnListView.itemsSource = _validations;
_verifyListMultiColumnListView.RefreshItems();
_verifyFixButton.SetEnabled(_validations.Any());
}
private void OnVerifyFix()
{
ValidationSolver.FixAll(_validations);
_verifyListMultiColumnListView.RefreshItems();
}
private void OnVerifyClear()
{
_validations.Clear();
_verifyListMultiColumnListView.RefreshItems();
}
#endregion
#region MODULES
private void CreateModulesList()
{
_modulesListScrollView.Clear();
_moduleTypes.Select(CreateModuleRow).ForEach(_modulesListScrollView.Add);
}
private VisualElement CreateModuleRow(Type moduleType)
{
var objectType = moduleType.FindGenericDefinitionType(typeof(ObjectModule<>)).GetGenericArguments()[0];
var moduleAttribute = moduleType.GetCustomAttribute<ObjectModuleAttribute>();
var moduleMonoScript = _moduleMonoScripts.GetValueOrDefault(moduleType);
var foldout = new Foldout();
foldout.AddToClassList("module-foldout");
foldout.value = false;
var moduleName = $"<b>{moduleAttribute?.name ?? moduleType.Name}</b>";
var assemblyName = $"<alpha=\"#69\">asm: {moduleType.Assembly.GetName().Name}</alpha>";
foldout.text = moduleName + " " + assemblyName;
var objectField = new ObjectField { objectType = typeof(MonoScript), value = moduleMonoScript, enabledSelf = false };
foldout.Add(objectField);
var descriptionLabel = new Label(moduleAttribute?.description ?? $"// {nameof(ObjectModuleAttribute)} not found on {moduleType.Name} //");
descriptionLabel.style.whiteSpace = WhiteSpace.Normal;
foldout.Add(descriptionLabel);
var usageTreeElement = CreateObjectTreeUsageForModule(moduleType, objectType, moduleAttribute?.required ?? false);
foldout.Add(usageTreeElement);
return foldout;
}
private VisualElement CreateObjectTreeUsageForModule(Type moduleType, Type objectType, bool isRequired, string prefix = "")
{
var config = EditorConfigSO.instance;
var isEnabled = config.modules?.GetValueOrDefault(moduleType)?.Any(st => st.type == objectType) ?? false;
var section = new VisualElement();
if (prefix == "")
{
var sectionTitle = new Label();
sectionTitle.style.marginTop = 8;
sectionTitle.text = "<u>Modules enabled by default:</u>";
section.Add(sectionTitle);
}
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
section.Add(row);
var rowPrefixPipe = new Label(prefix);
rowPrefixPipe.style.whiteSpace = WhiteSpace.Pre;
row.Add(rowPrefixPipe);
var rowModuleToggle = new Toggle(objectType.Name);
rowModuleToggle.name = $"module-{moduleType.Name}-{objectType.Name}";
rowModuleToggle.value = isRequired || isEnabled;
rowModuleToggle.SetEnabled(!isRequired);
rowModuleToggle.RegisterValueChangedCallback(ev => UpdateModuleUsage(moduleType, objectType, ev.newValue));
row.Add(rowModuleToggle);
if (!_objectTypesMap.TryGetValue(objectType, out var subTypes)) return section;
prefix = prefix.Replace(PipeStart, PipeGo).Replace(PipeEnd, PipeNone);
for (var i = 0; i < subTypes.Count; i++)
{
var subPrefix = prefix + (i == subTypes.Count-1 ? PipeEnd : PipeStart);
var subType = subTypes[i];
var subSection = CreateObjectTreeUsageForModule(moduleType, subType, isRequired || isEnabled, subPrefix);
section.Add(subSection);
}
return section;
}
private void UpdateModuleUsage(Type moduleType, Type objectType, bool isRequired)
{
var toggleName = $"module-{moduleType.Name}-{objectType.Name}";
var toggle = _modulesListScrollView.Q<Toggle>(toggleName);
toggle.parent.parent
.Query<Toggle>()
.ToList()
.Where(subToggle => subToggle != toggle)
.ForEach(subToggle =>
{
subToggle.SetValueWithoutNotify(false);
subToggle.SetEnabled(!isRequired);
});
var config = EditorConfigSO.instance;
config.modules[moduleType].RemoveAll(t => objectType.IsAssignableFrom(t));
if (isRequired) config.modules[moduleType].Add(objectType);
config.Save();
}
private void OnModuleFilter(string filter)
{
var foldouts = _modulesListScrollView.Query<Foldout>(className: "module-foldout").ToList();
foreach (var foldout in foldouts)
{
var matchFilter = foldout.text.Contains(filter, StringComparison.InvariantCultureIgnoreCase);
foldout.style.display = matchFilter ? DisplayStyle.Flex : DisplayStyle.None;
foldout.value = !string.IsNullOrEmpty(filter) && matchFilter;
}
}
#endregion
}
}