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
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3bc3378d3d4847e3b8a522fce249109b
timeCreated: 1780745996
@@ -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
}
}
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 75b5674e6448496f8272962a2786cf84
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- m_ViewDataDictionary: {instanceID: 0}
- _visualTreeAsset: {fileID: 9197481963319205126, guid: 3407a07e2ddf46b9abc87c2f32b2efb0, type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,110 @@
TabView {
flex-grow: 1;
}
TabView #unity-tab-view__header-container {
flex-grow: 1;
}
TabView #unity-tab__header {
flex-grow: 1;
}
TabView #unity-tab__header-label {
flex-grow: 1;
-unity-text-align: upper-center;
}
TabView #unity-tab-view__content-container {
flex-grow: 1;
}
TabView > Tab {
flex-grow: 1;
}
TabView > Tab > #unity-tab__content-container {
flex-grow: 1;
}
Toolbar > Label {
-unity-text-align: middle-left;
}
Toolbar > ToolbarSpacer {
flex-grow: 1;
}
Toolbar > ToolbarButton {
padding-left: 15px;
padding-right: 15px;
}
#db-hello {
}
#db-hello > #db-hello-title {
-unity-text-align: upper-center;
-unity-font-style: bold;
font-size: 36px;
margin-top: 50px;
}
#db-hello > #db-hello-description {
-unity-text-align: upper-center;
-unity-font-style: normal;
font-size: 20px;
margin-bottom: 50px;
}
#db-verify {
flex-direction: column;
}
#db-verify > #db-verify-list {
flex-grow: 1;
}
#db-verify > #db-verify-list .unity-multi-column-view__cell > * {
margin-right: 5px;
margin-left: 5px;
overflow: hidden;
}
#db-verify > #db-verify-list .verify-success {
color: rgb(0, 255, 0);
}
#db-verify > #db-verify-list .verify-error {
color: rgb(255, 69, 0);
}
#db-modules > #db-modules-list > Foldout {
padding-top: 4px;
padding-right: 4px;
padding-bottom: 4px;
padding-left: 4px;
}
#db-modules > #db-modules-list > Foldout:hover {
background-color: rgba(0, 0, 0, 0.1);
}
#db-modules > #db-modules-list > Foldout > Toggle {
padding-top: 2px;
padding-right: 2px;
padding-bottom: 2px;
padding-left: 2px;
background-color: rgba(0, 0, 0, 0.3);
border-top-left-radius: 2px;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
border-bottom-left-radius: 2px;
}
#db-modules > #db-modules-list > Foldout > Toggle Label {
}
#db-modules > #db-modules-list > Foldout > .unity-foldout__content {
margin-bottom: 4px;
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5310fd50f52f49e3aeb54cbd15bc986f
timeCreated: 1780746434
@@ -0,0 +1,40 @@
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<Style src="project://database/Assets/TheVVaS/RPGCore/Editor/CoreManager/DashboardWindow.uss?fileID=7433441132597879392&amp;guid=5310fd50f52f49e3aeb54cbd15bc986f&amp;type=3#DashboardWindow"/>
<ui:TabView name="db-root">
<ui:Tab label="Hello" name="db-hello">
<ui:Label text="RPGCORE" name="db-hello-title"/>
<ui:Label text="RPGCORE" name="db-hello-description"/>
</ui:Tab>
<ui:Tab label="Verify" name="db-verify">
<uie:Toolbar name="">
<ui:Label text="Warnings about component usages:"/>
<uie:ToolbarSpacer/>
<uie:ToolbarButton text="Check" name="db-verify-check"/>
<uie:ToolbarButton text="Fix all" name="db-verify-fix"/>
<uie:ToolbarButton text="Clear" name="db-verify-clear"/>
</uie:Toolbar>
<ui:MultiColumnListView columns="" sort-column-descriptions="" name="db-verify-list">
<ui:Columns reorderable="false" primary-column-name="validator">
<ui:Column name="validator" title="Validator" sortable="false" optional="false" stretchable="true"/>
<ui:Column name="message" title="Message" sortable="false" optional="false" stretchable="true"/>
<ui:Column name="context" title="File" sortable="false" optional="false" stretchable="true"/>
<ui:Column name="hierarchy" title="Hierarchy" sortable="false" optional="false" stretchable="true"/>
<ui:Column name="result" title="Result" stretchable="true" sortable="false" optional="false" visible="true"/>
</ui:Columns>
</ui:MultiColumnListView>
</ui:Tab>
<ui:Tab label="Modules" name="db-modules">
<uie:Toolbar name="">
<uie:ToolbarSearchField name="db-modules-filter"/>
<uie:ToolbarSpacer/>
<uie:ToolbarButton text="Button"/>
</uie:Toolbar>
<ui:ScrollView name="db-modules-list"/>
</ui:Tab>
<ui:Tab label="Config" name="db-config">
<ui:ScrollView>
<uie:PropertyField name="db-config-verify-paths"/>
</ui:ScrollView>
</ui:Tab>
</ui:TabView>
</ui:UXML>
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3407a07e2ddf46b9abc87c2f32b2efb0
timeCreated: 1780746434
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using RPGCoreCommon.Helpers.CustomTypes;
using UnityEditor;
using UnityEngine;
using UnityEngine.Search;
using Object = UnityEngine.Object;
namespace RPGCore.Editor.CoreManager
{
[Serializable]
[FilePath("Assets/TheVVaS/RPGCore/Editor/CoreManager/EditorConfigSO.asset", FilePathAttribute.Location.ProjectFolder)]
internal class EditorConfigSO : ScriptableSingleton<EditorConfigSO>
{
[SerializeField]
internal SerializableDictionary<SerializableType, List<SerializableType>> modules = new();
[SerializeField]
internal string[] verifyPaths;
internal void Save() => base.Save(true);
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ae589e57361f4f7a8a3c978f5ed6b14e
timeCreated: 1781360025
+14
View File
@@ -0,0 +1,14 @@
using RPGCore.Editor.CoreManager.Validators;
namespace RPGCore.Editor.CoreManager
{
internal class Validation
{
internal IValidator validator;
internal string message;
internal string contextPath;
internal string hierarchyPath;
internal bool? result;
internal string resultMessage;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6cf1ca955da24272b089bf08c86f39f4
timeCreated: 1780839857
@@ -0,0 +1,238 @@
using System;
using System.Collections.Generic;
using System.Linq;
using RPGCore.Editor.CoreManager.Validators;
using RPGCoreCommon.Helpers;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace RPGCore.Editor.CoreManager
{
internal static class ValidationSolver
{
private static readonly List<IValidator> Validators =
TypeCache.GetTypesDerivedFrom<IValidator>()
.Select(Activator.CreateInstance)
.Cast<IValidator>()
.ToList();
[InitializeOnLoadMethod]
private static void AutomaticValidation_Initialize() => ObjectChangeEvents.changesPublished += AutomaticValidation;
private static void AutomaticValidation(ref ObjectChangeEventStream stream)
{
var instanceIds = new List<int>();
for (var i = 0; i < stream.length; i++)
{
if (stream.GetEventType(i) != ObjectChangeKind.ChangeGameObjectStructure) continue;
stream.GetChangeGameObjectStructureEvent(0, out var ev);
instanceIds.Add(ev.instanceId);
}
if (instanceIds.Count == 0) return;
var validations = Check(instanceIds);
FixAll(validations);
// Can't finish because of exception or error
validations
.Where(v => v.result == false)
.Select(v => "--- ERROR: " + v.resultMessage +
"\n> " + v.contextPath +
"\n> " + v.hierarchyPath +
"\n> " + v.validator.GetType().Name +
"\n> " + v.message)
.ForEach(Debug.LogError);
// No fix defined
validations
.Where(v => v.result == null)
.Select(v => "--- WARNING: " + v.resultMessage +
"\n> " + v.contextPath +
"\n> " + v.hierarchyPath +
"\n> " + v.validator.GetType().Name +
"\n> " + v.message)
.ForEach(Debug.LogWarning);
}
internal static List<Validation> Check(List<int> instanceIds)
{
var validations = new List<Validation>();
foreach (var instanceId in instanceIds)
{
var obj = EditorUtility.EntityIdToObject(instanceId);
if (obj is not GameObject go) continue;
Check(go, validations, false);
}
return validations;
}
internal static List<Validation> Check(string[] paths)
{
var validations = new List<Validation>();
foreach (var guid in AssetDatabase.FindAssetGUIDs("t:Scene", paths))
{
var scene = EditorSceneManager.OpenPreviewScene(AssetDatabase.GUIDToAssetPath(guid));
foreach (var go in scene.GetRootGameObjects())
{
try
{
Check(go, validations, true);
}
catch (Exception)
{
// ignored
}
}
EditorSceneManager.ClosePreviewScene(scene);
}
foreach (var guid in AssetDatabase.FindAssetGUIDs("t:Prefab", paths))
{
var prefabPath = AssetDatabase.GUIDToAssetPath(guid);
var prefab = PrefabUtility.LoadPrefabContents(prefabPath);
try
{
Check(prefab, validations, true);
}
catch (Exception)
{
// ignored
}
PrefabUtility.UnloadPrefabContents(prefab);
}
return validations;
}
internal static void Check(GameObject go, List<Validation> result, bool recursive)
{
// On scene and part of other prefab - don't validate that, it will be validated separately anyway
if (PrefabUtility.IsPartOfPrefabInstance(go)) return;
foreach (var validator in Validators)
{
var message = validator.Check(go);
if (string.IsNullOrEmpty(message)) continue;
result.Add(new Validation
{
validator = validator,
message = message,
contextPath = GetContextPath(go),
hierarchyPath = GetHierarchyPath(go),
});
}
if (!recursive) return;
for (var i = 0; i < go.transform.childCount; i++)
Check(go.transform.GetChild(i).gameObject, result, true);
}
internal static void FixAll(List<Validation> validations)
{
foreach (var validation in validations)
{
var contextType = AssetDatabase.GetMainAssetTypeAtPath(validation.contextPath);
if (contextType == typeof(SceneAsset)) FixOnScene(validation);
else if (contextType == typeof(GameObject)) FixOnPrefab(validation);
else throw new Exception($"Given file '{validation.contextPath}' is not prefab nor scene!");
}
}
private static void FixOnScene(Validation validation)
{
var scene = Enumerable.Range(0, SceneManager.loadedSceneCount)
.Select(SceneManager.GetSceneAt)
.FirstOrDefault(scene => scene.path == validation.contextPath);
var isActive = scene.IsValid();
if (!isActive) scene = EditorSceneManager.OpenPreviewScene(validation.contextPath);
var go = FindGameObject(scene.GetRootGameObjects(), validation);
FixGameObject(go, validation);
if (isActive) EditorUtility.SetDirty(go);
if (!isActive) EditorSceneManager.SaveScene(scene);
if (!isActive) EditorSceneManager.ClosePreviewScene(scene);
}
private static void FixOnPrefab(Validation validation)
{
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
var isActive = prefabStage && prefabStage.assetPath == validation.contextPath;
var prefabRoot = isActive
? prefabStage.prefabContentsRoot
: PrefabUtility.LoadPrefabContents(validation.contextPath);
var go = FindGameObject(new[] { prefabRoot }, validation);
FixGameObject(go, validation);
if (isActive) EditorUtility.SetDirty(go);
if (!isActive) PrefabUtility.SaveAsPrefabAsset(prefabRoot, validation.contextPath);
if (!isActive) PrefabUtility.UnloadPrefabContents(prefabRoot);
}
private static GameObject FindGameObject(GameObject[] roots, Validation validation)
{
var hierarchyPathArray = validation.hierarchyPath.Split('/', 2);
var root = roots.FirstOrDefault(go => go.name == hierarchyPathArray[0]);
if (!root) throw new Exception($"Root '{hierarchyPathArray[0]}' not found in '{validation.contextPath}'!");
var go = hierarchyPathArray.Length > 1 ? root.transform.Find(hierarchyPathArray[1])?.gameObject : root;
if (!go) throw new Exception($"GameObject '{validation.hierarchyPath}' not found in '{validation.contextPath}'!");
return go;
}
private static void FixGameObject(GameObject go, Validation validation)
{
try
{
validation.validator.Fix(go);
validation.resultMessage = "Done.";
validation.result = true;
}
catch (NotImplementedException)
{
validation.resultMessage = "No Auto-fix defined. Do it yourself.";
}
catch (Exception e)
{
validation.result = false;
validation.resultMessage = e.Message;
}
}
private static string GetContextPath(GameObject go)
{
// ON PREFAB STAGE - this one will be used only in realtime correcting, never in verify tab
if (PrefabStageUtility.GetPrefabStage(go) is {} stage) return stage.assetPath;
// ON SCENE
if (go.scene.IsValid()) return go.scene.path;
// PREFAB ASSET
if (PrefabUtility.IsPartOfPrefabAsset(go)) return go.scene.path;
throw new Exception("No valid context path found, sad pepe :(");
}
private static string GetHierarchyPath(GameObject go)
{
var path = new List<string>();
while (go is not null)
{
path.Add(go.name);
go = go.transform.parent?.gameObject;
}
return string.Join("/", path.AsEnumerable().Reverse());
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b0debb69c5494d57afaa2f4ae614e28b
timeCreated: 1780746179
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 013142cbe59742f3a74785de7737c679
timeCreated: 1780836497
@@ -0,0 +1,12 @@
using System;
using UnityEngine;
namespace RPGCore.Editor.CoreManager.Validators
{
internal interface IValidator
{
internal string title { get; }
internal string Check(GameObject gameObject);
internal void Fix(GameObject gameObject) => throw new NotImplementedException();
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bf525989f81247dfb51cd609a3dd659b
timeCreated: 1780761759
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using RPGCore.Core;
using RPGCore.Core.Objects;
using RPGCoreCommon.Helpers;
using UnityEditor;
using UnityEngine;
namespace RPGCore.Editor.CoreManager.Validators
{
internal class MissingModuleValidator : IValidator
{
// ObjectType => List of ModuleType (only required by config)
private static readonly Dictionary<Type, List<Type>> RequiredModulesMap =
TypeCache.GetTypesDerivedFrom<ObjectModule>()
.Where(t => !t.IsAbstract && !t.IsInterface)
.GroupBy(t => t.FindGenericDefinitionType(typeof(ObjectModule<>)).GetGenericArguments()[0])
.ToDictionary(
g => g.Key,
g => g.ToList().Where(t => IsModuleRequired(t, g.Key)).ToList()
);
string IValidator.title => "Adding missing modules";
private static bool IsModuleRequired(Type moduleType, Type objectType)
{
return EditorConfigSO.instance.modules.GetValueOrDefault(moduleType)?.Contains(objectType) ?? false;
}
private static List<Type> FindMissingModules(GameObject gameObject)
{
var objectType = gameObject.GetComponent<BaseObject>()?.GetType();
if (objectType == null) return null;
return RequiredModulesMap
.Where(pair => objectType.IsAssignableFrom(pair.Key))
.SelectMany(pair => pair.Value)
.Where(t => !gameObject.GetComponent(t))
.ToList();
}
string IValidator.Check(GameObject gameObject)
{
var missingModules = FindMissingModules(gameObject);
if (missingModules.Count < 1) return null;
return "Missing modules: " + missingModules.Select(m => m.Name).StringJoin(", ");
}
void IValidator.Fix(GameObject gameObject)
{
var missingModules = FindMissingModules(gameObject);
if (missingModules.Count < 1) return;
missingModules.ForEach(m => gameObject.AddComponent(m));
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 117ff9ca9b964927a0482806c91a75d7
timeCreated: 1780846792
@@ -0,0 +1,19 @@
using System.Linq;
using RPGCore.Core.Objects;
using RPGCoreCommon.Helpers;
using UnityEngine;
namespace RPGCore.Editor.CoreManager.Validators
{
internal class MultipleObjectsValidator : IValidator
{
string IValidator.title => "Removing duplicated ObjectTypes";
string IValidator.Check(GameObject gameObject)
{
var duplicates = gameObject.GetComponents<BaseObject>().Select(c => c.GetType().Name).ToList();
if (duplicates.Count <= 1) return null;
return "Multiple <b>ObjectTypes</b> on single GameObject: " + duplicates.StringJoin(", ");
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9236817ce90b46978bc2ca4b8d042db1
timeCreated: 1781469084
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using RPGCore.Core;
using RPGCore.Core.Objects;
using RPGCoreCommon.Helpers;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace RPGCore.Editor.CoreManager.Validators
{
internal class OrphanModuleValidator : IValidator
{
private static readonly Dictionary<Type, List<Type>> ModulesMap =
TypeCache.GetTypesDerivedFrom<ObjectModule>()
.Where(t => !t.IsAbstract && !t.IsInterface)
.GroupBy(t => t.FindGenericDefinitionType(typeof(ObjectModule<>)).GetGenericArguments()[0])
.ToDictionary(g => g.Key, g => g.ToList());
string IValidator.title => "Removing orphaned modules";
private static List<ObjectModule> FindOrphanModules(GameObject gameObject)
{
var objectType = gameObject.GetComponent<BaseObject>()?.GetType();
var validModuleTypes = ModulesMap
.Where(pair => pair.Key.IsAssignableFrom(objectType))
.SelectMany(pair => pair.Value)
.ToList();
return gameObject
.GetComponents<ObjectModule>()
.Where(m => !validModuleTypes.Contains(m.GetType()))
.ToList();
}
string IValidator.Check(GameObject gameObject)
{
var orphanModules = FindOrphanModules(gameObject);
if (orphanModules.Count < 1) return null;
return "Found invalid modules: " + orphanModules.Select(m => m.GetType().Name).StringJoin(", ");
}
void IValidator.Fix(GameObject gameObject)
{
var orphanModules = FindOrphanModules(gameObject);
if (orphanModules.Count < 1) return;
orphanModules.ForEach(Object.DestroyImmediate);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7e12170f343d4538ad98f50ec31325c1
timeCreated: 1781464441
-15
View File
@@ -1,15 +0,0 @@
using RPGCore.Core.Objects;
using UnityEngine;
namespace RPGCore.Core
{
/// <summary>
/// This interface allow any <see cref="BaseObject"/> to call <see cref="OnEnter"/> and <see cref="OnExit"/>.<br/>
/// Setting <see cref="Collider"/>'s <see cref="Collider.isTrigger"/> to true is required!<br/>
/// </summary>
public interface ITrigger
{
public void OnEnter(BaseObject obj);
public void OnExit(BaseObject obj);
}
}
@@ -0,0 +1,19 @@
using System;
namespace RPGCore.Core
{
[AttributeUsage(AttributeTargets.Class)]
public class ObjectModuleAttribute : Attribute
{
public string name;
public string description;
public bool required;
public ObjectModuleAttribute(string name, string description, bool required = false)
{
this.name = name;
this.description = description;
this.required = required;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7732c0ab6a6148ddb8c17a80facf5204
timeCreated: 1780777138
+12 -9
View File
@@ -13,13 +13,11 @@ namespace RPGCore.Core.Objects
[DisallowMultipleComponent]
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(EventModule))]
[RequireComponent(typeof(ActionModule))]
[RequireComponent(typeof(DataModule))]
public abstract class BaseObject : MonoBehaviour
{
[field: SerializeField, ReadOnly] public new Rigidbody rigidbody { get; private set; }
[field: SerializeField, ReadOnly] public EventModule events { get; private set; }
[field: SerializeField, ReadOnly] public ActionModule actions { get; private set; }
[field: SerializeField, ReadOnly] public DataModule data { get; private set; }
[DynamicValueProvider]
@@ -34,7 +32,6 @@ namespace RPGCore.Core.Objects
{
rigidbody = GetComponent<Rigidbody>();
events = GetComponent<EventModule>();
actions = GetComponent<ActionModule>();
data = GetComponent<DataModule>();
GetComponents<ObjectModule>().ForEach(module => module.parent = this);
}
@@ -42,25 +39,31 @@ namespace RPGCore.Core.Objects
/// <summary>Removes this object from game.</summary>
public void Remove()
{
events.Invoke(new TriggerClearEvent { obj = this });
events.Invoke(new RemoveEvent{ obj = this });
Destroy(gameObject);
}
public void OnDisable()
{
events.Invoke(new TriggerClearEvent { obj = this });
}
/// <summary>It'll execute <see cref="ITrigger"/>.<see cref="ITrigger.OnEnter"/> when this object enter its collider.</summary>
/// <summary>It'll execute <see cref="Trigger"/>.<see cref="Trigger.OnEnter"/> when this object enter its collider.</summary>
private void OnTriggerEnter(Collider other)
{
other.GetComponentsInParent<ITrigger>().ForEach(trigger => {
other.GetComponentsInParent<Trigger>().ForEach(trigger => {
trigger.OnEnter(this);
events.Invoke(new TriggerEnterEvent { target = this, trigger = trigger });
events.Invoke(new TriggerEnterEvent { obj = this, trigger = trigger });
});
}
/// <summary>It'll execute <see cref="ITrigger"/>.<see cref="ITrigger.OnExit"/> when this object exit its collider.</summary>
/// <summary>It'll execute <see cref="Trigger"/>.<see cref="Trigger.OnExit"/> when this object exit its collider.</summary>
private void OnTriggerExit(Collider other)
{
other.GetComponentsInParent<ITrigger>().ForEach(trigger => {
other.GetComponentsInParent<Trigger>().ForEach(trigger => {
trigger.OnExit(this);
events.Invoke(new TriggerExitEvent { target = this, trigger = trigger });
events.Invoke(new TriggerExitEvent { obj = this, trigger = trigger });
});
}
}
@@ -1,4 +1,5 @@
using System;
using RPGCore.ObjectModules.ActionObjectModule;
using RPGCoreCommon.DynamicValues;
using RPGCoreCommon.Helpers.PropertyAttributeDrawers;
using UnityEngine;
@@ -6,16 +7,19 @@ using UnityEngine;
namespace RPGCore.Core.Objects
{
[RequireComponent(typeof(CapsuleCollider))]
[RequireComponent(typeof(ActionModule))]
public class UnitObject : BaseObject
{
[DynamicValueProvider]
private ObjectModule<UnitObject> UnitModuleProvider(Type moduleType) => GetComponent(moduleType) as ObjectModule<UnitObject>;
[field: SerializeField, ReadOnly] public CapsuleCollider unitCollider { get; private set; }
[field: SerializeField, ReadOnly] public ActionModule actions { get; private set; }
protected new void OnValidate()
{
base.OnValidate();
actions = GetComponent<ActionModule>();
unitCollider = GetComponent<CapsuleCollider>();
}
}
+69
View File
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using RPGCore.Core.Objects;
using RPGCore.ObjectModules.EventObjectModule.Events;
using RPGCoreCommon.Settings;
using UnityEngine;
namespace RPGCore.Core
{
/// <summary>
/// This MonoBehaviour allow any <see cref="BaseObject"/> to call <see cref="OnEnter"/> and <see cref="OnExit"/>.<br/>
/// Setting <see cref="Collider"/>'s <see cref="Collider.isTrigger"/> to true is required!<br/>
/// </summary>
public class Trigger : MonoBehaviour
{
private event Action<BaseObject> OnEnterEvent;
private event Action<BaseObject> OnExitEvent;
private readonly Dictionary<BaseObject, int> _collisionCounter = new();
public static Trigger AddToGameObject(GameObject go, Action<BaseObject> onEnter, Action<BaseObject> onExit)
{
go.layer = SettingsManager.Get<CoreSettings>().triggerLayer;
var interactTrigger = go.AddComponent<Trigger>();
interactTrigger.OnEnterEvent += onEnter;
interactTrigger.OnExitEvent += onExit;
return interactTrigger;
}
internal void OnEnter(BaseObject obj)
{
var alreadyTouching = _collisionCounter.ContainsKey(obj);
if (!alreadyTouching) _collisionCounter.Add(obj, 0);
_collisionCounter[obj]++;
if (!alreadyTouching)
{
obj.events.Register<TriggerClearEvent>(OnClear);
OnEnterEvent?.Invoke(obj);
}
}
internal void OnExit(BaseObject obj)
{
if (!_collisionCounter.ContainsKey(obj))
{
Debug.LogError("Object tried to leave trigger, but never entered it!", obj);
return;
}
_collisionCounter[obj]--;
if (_collisionCounter[obj] == 0) Clear(obj);
}
private void OnClear(TriggerClearEvent ev)
{
Clear(ev.obj);
}
private void Clear(BaseObject obj)
{
obj.events.Unregister<TriggerClearEvent>(OnClear);
_collisionCounter.Remove(obj);
OnExitEvent?.Invoke(obj);
}
}
}
@@ -11,13 +11,19 @@ using UnityEngine;
namespace RPGCore.ObjectModules.ActionObjectModule
{
[Serializable]
[ObjectModule(
name: "[Core] Actions",
description: "[Built-in module] Module that can be attached to <b>UnitObject</b>. " +
"Can execute any implementations of <b>BaseAction</b> with queue and/or instant execution.",
required: true
)]
public class ActionModule : ObjectModule<UnitObject>
{
private List<BaseActionParallel> _actionParallels = new();
private List<BaseAction> _actionParallels = new();
private readonly List<BaseAction> _actionQueue = new();
private BaseAction _actionCurrent;
public List<BaseActionParallel> actionParallels => _actionParallels.ToList();
public List<BaseAction> actionParallels => _actionParallels.ToList();
public List<BaseAction> actionQueue => _actionQueue.ToList();
public BaseAction actionCurrent => _actionCurrent;
@@ -30,7 +36,7 @@ namespace RPGCore.ObjectModules.ActionObjectModule
/// 1. Checks if the action can be executed outside the queue.<br/>
/// 2. If allowed, executes the action and handles potential errors.
/// </summary>
public void Execute(BaseActionParallel action)
public void Execute(BaseAction action)
{
try
{
@@ -1,7 +0,0 @@
namespace RPGCore.ObjectModules.ActionObjectModule
{
public abstract class BaseActionParallel : BaseAction
{
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: e46b8a02e3894ecb87b4455602287715
timeCreated: 1762624491
@@ -8,7 +8,7 @@ namespace RPGCore.ObjectModules.EventObjectModule.Data
[Serializable]
public class BaseObjectData : BaseData<BaseObject>
{
[Header("Uniqueness")]
[field: Header("Uniqueness")]
[field: SerializeField, ReadOnly] public string guid { get; private set; } = Guid.NewGuid().ToString();
[Header("Display")]
@@ -8,7 +8,12 @@ using UnityEngine;
namespace RPGCore.ObjectModules.EventObjectModule
{
[Serializable]
[RequireComponent(typeof(BaseObject))]
[ObjectModule(
name: "[Core] Data",
description: "[Built-in module] Manages all implementations of <b>BaseData</b>. " +
"Automatically adds matching data to attached implementation of <b>BaseObject</b>.",
required: true
)]
public class DataModule : ObjectModule<BaseObject>
{
// SERIALIZED
@@ -48,6 +53,9 @@ namespace RPGCore.ObjectModules.EventObjectModule
public T Get<T>() where T : BaseData
{
if (!_dataDict.ContainsKey(typeof(T)))
_dataDict.Add(typeof(T), Activator.CreateInstance(typeof(T)) as T);
return (T)_dataDict[typeof(T)];
}
}
@@ -2,18 +2,28 @@
using System.Collections.Generic;
using RPGCore.Core;
using RPGCore.Core.Objects;
using UnityEngine;
namespace RPGCore.ObjectModules.EventObjectModule
{
[Serializable]
[ObjectModule(
name: "[Core] Data",
description: "[Built-in module] Manages all implementations of <b>BaseEvent</b> and <b>BasePreventableEvent</b>. " +
"Allows for registering and invoking events by its type.",
required: true
)]
public class EventModule : ObjectModule<BaseObject>
{
// TODO: tutaj dodać serializowane eventy w postaci SerializableDictionary<SerializableType, UnityAction>
// TODO: dodatkowo mozna zmienić preventableEvents na dwa osobne słowniki before i after
// TODO: 1. tutaj dodać serializowane eventy w postaci SerializableDictionary<SerializableType, UnityAction>
// TODO: w Awake() zrobić bridge to uruchamiania serializowanych eventów jak uruchamia się ten runtimeowy
// TODO: 2. dodatkowo mozna zmienić preventableEvents na dwa osobne słowniki before i after
internal readonly Dictionary<Type, Delegate> events = new();
internal readonly Dictionary<Type, Delegate[]> preventableEvents = new();
/***** INVOKE *****/
public void Invoke<T>(T baseEvent) where T : BaseEvent
{
events.TryAdd(typeof(T), null);
@@ -23,19 +33,17 @@ namespace RPGCore.ObjectModules.EventObjectModule
public void Register<T>(Action<T> action) where T : BaseEvent
{
events.TryAdd(typeof(T), null);
var temp = (Action<T>)events[typeof(T)];
temp += action;
events[typeof(T)] = temp;
events[typeof(T)] = (Action<T>)events[typeof(T)] + action;
}
public void Unregister<T>(Action<T> action) where T : BaseEvent
{
events.TryAdd(typeof(T), null);
var temp = (Action<T>)events[typeof(T)];
temp -= action;
events[typeof(T)] = temp;
events[typeof(T)] = (Action<T>)events[typeof(T)] - action;
}
/***** INVOKE BEFORE *****/
public void InvokeBefore<T>(T basePreventableEvent) where T : BasePreventableEvent
{
preventableEvents.TryAdd(typeof(T), new Delegate[2]);
@@ -45,22 +53,25 @@ namespace RPGCore.ObjectModules.EventObjectModule
public void RegisterBefore<T>(Action<T> action) where T : BasePreventableEvent
{
preventableEvents.TryAdd(typeof(T), new Delegate[2]);
var temp = (Action<T>)preventableEvents[typeof(T)][0];
temp += action;
preventableEvents[typeof(T)][0] = temp;
preventableEvents[typeof(T)][0] = (Action<T>)preventableEvents[typeof(T)][0] + action;
}
public void UnregisterBefore<T>(Action<T> action) where T : BasePreventableEvent
{
preventableEvents.TryAdd(typeof(T), new Delegate[2]);
var temp = (Action<T>)preventableEvents[typeof(T)][0];
temp -= action;
preventableEvents[typeof(T)][0] = temp;
preventableEvents[typeof(T)][0] = (Action<T>)preventableEvents[typeof(T)][0] - action;
}
/***** INVOKE AFTER *****/
public void InvokeAfter<T>(T basePreventableEvent) where T : BasePreventableEvent
{
if (basePreventableEvent.isPrevented) throw new EventPreventedException("Event is prevented and can't be invoked!");
if (basePreventableEvent.isPrevented)
{
Debug.LogWarning("Event is prevented and can't be invoked!");
return;
}
preventableEvents.TryAdd(typeof(T), new Delegate[2]);
(preventableEvents[typeof(T)][1] as Action<T>)?.Invoke(basePreventableEvent);
}
@@ -68,17 +79,13 @@ namespace RPGCore.ObjectModules.EventObjectModule
public void RegisterAfter<T>(Action<T> action) where T : BasePreventableEvent
{
preventableEvents.TryAdd(typeof(T), new Delegate[2]);
var temp = (Action<T>)preventableEvents[typeof(T)][1];
temp += action;
preventableEvents[typeof(T)][1] = temp;
preventableEvents[typeof(T)][1] = (Action<T>)preventableEvents[typeof(T)][1] + action;
}
public void UnregisterAfter<T>(Action<T> action) where T : BasePreventableEvent
{
preventableEvents.TryAdd(typeof(T), new Delegate[2]);
var temp = (Action<T>)preventableEvents[typeof(T)][1];
temp -= action;
preventableEvents[typeof(T)][1] = temp;
preventableEvents[typeof(T)][1] = (Action<T>)preventableEvents[typeof(T)][1] - action;
}
}
}
@@ -1,11 +0,0 @@
using System;
namespace RPGCore.ObjectModules.EventObjectModule
{
public class EventPreventedException : Exception
{
public EventPreventedException(string message) : base(message)
{
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: a647e5d4d4ed44c28fd6961d60943fe4
timeCreated: 1761917482
@@ -0,0 +1,15 @@
using RPGCore.Core;
using RPGCore.Core.Objects;
namespace RPGCore.ObjectModules.EventObjectModule.Events
{
/// <summary>
/// Executed when <see cref="BaseObject"/> forcefully exits <see cref="Trigger"/> by non-physics means, by default
/// it is when object is <b>Disabled</b> or <b>Removed</b>.<br/><br/>
/// Execute this if you want to object exit its trigger.
/// </summary>
public class TriggerClearEvent : BaseEvent<BaseObject>
{
public BaseObject obj;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4b02fda53526495c88e8c59795782dec
timeCreated: 1779650139
@@ -3,9 +3,12 @@ using RPGCore.Core.Objects;
namespace RPGCore.ObjectModules.EventObjectModule.Events
{
/// <summary>
/// Executed when <see cref="BaseObject"/> enters <see cref="Trigger"/>'s colliders.
/// </summary>
public class TriggerEnterEvent : BaseEvent<BaseObject>
{
public BaseObject target;
public ITrigger trigger;
public BaseObject obj;
public Trigger trigger;
}
}
@@ -3,9 +3,12 @@ using RPGCore.Core.Objects;
namespace RPGCore.ObjectModules.EventObjectModule.Events
{
/// <summary>
/// Executed when <see cref="BaseObject"/> exits <see cref="Trigger"/>'s colliders.
/// </summary>
public class TriggerExitEvent : BaseEvent<BaseObject>
{
public BaseObject target;
public ITrigger trigger;
public BaseObject obj;
public Trigger trigger;
}
}