Files
TheVVaS-Assets/RPGCoreCommon/Settings/Editor/SettingsEditorWindow.cs
T
2026-04-25 23:37:10 +02:00

278 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using Object = UnityEngine.Object;
namespace RPGCoreCommon.Settings.Editor
{
internal class SettingsEditorWindow : EditorWindow
{
[SerializeField] private VisualTreeAsset _visualTreeAsset;
private const string FolderName = "_CustomSettings";
private const string FolderPath = "Assets/" + FolderName;
private const string CreateExampleContent = @"using RPGCoreCommon.Settings;
[CustomSettings(""Example Custom Settings"")]
public class ExampleCustomSettingsSO : CustomSettingsSO
{
public string exampleText;
public int exampleNumber;
}
";
// ELEMENTS
private ScrollView _listElement;
private ObjectField _objectField;
private InspectorElement _inspectorElement;
private VisualElement _errorElement;
private VisualElement _missingElement;
private Button _reloadDomainButton;
private Button _createExampleButton;
private Button _createMissingButton;
// SETTINGS
private MainSettingsSO _mainSettings;
private Dictionary<string, Type> _settingsTypeMap;
private string _currentKey;
[MenuItem("TheVVaS/RPGCore/Settings Editor")]
public static void ShowSettingsEditor()
{
var window = GetWindow<SettingsEditorWindow>();
window.titleContent = new GUIContent("Custom Settings Editor");
window.Show();
}
public override void SaveChanges()
{
_mainSettings.settings.ForEach(EditorUtility.SetDirty);
_mainSettings.settings.ForEach(AssetDatabase.SaveAssetIfDirty);
EditorUtility.SetDirty(_mainSettings);
AssetDatabase.SaveAssetIfDirty(_mainSettings);
hasUnsavedChanges = false;
}
private void CreateGUI()
{
_visualTreeAsset.CloneTree(rootVisualElement);
_listElement = rootVisualElement.Q<ScrollView>("cs-left");
_listElement.Clear();
_objectField = rootVisualElement.Q<ObjectField>("cs-settings-object");
_objectField.RegisterValueChangedCallback(ev => OnObjectChanged(ev.newValue));
_inspectorElement = rootVisualElement.Q<InspectorElement>("cs-settings-inspector");
_errorElement = rootVisualElement.Q<VisualElement>("cs-error");
_missingElement = rootVisualElement.Q<VisualElement>("cs-missing");
_reloadDomainButton = rootVisualElement.Q<Button>("cs-reload-domain");
_reloadDomainButton.clicked += EditorUtility.RequestScriptReload;
_createExampleButton = rootVisualElement.Q<Button>("cs-create-example");
_createExampleButton.clicked += CreateExampleSettings;
_createMissingButton = rootVisualElement.Q<Button>("cs-create-missing");
_createMissingButton.clicked += () => CreateMissingSettings(_settingsTypeMap[_currentKey]);
// Base info
_mainSettings = FindOrCreateMainSettings();
_settingsTypeMap = CreateSettingsTypeMap();
// No custom settings found - maybe user didn't create any yet? maybe domain is not reloaded? Show alternative
if (!_settingsTypeMap.Any())
{
_errorElement.style.display = DisplayStyle.Flex;
return;
}
// Select first settings available
PopulateList();
SelectKey(_settingsTypeMap.Keys.First());
}
private void PopulateList()
{
var orderedKeys = _settingsTypeMap.Keys.OrderBy(key => key.Count(s => s == '/')).ToList();
foreach (var key in orderedKeys)
{
var isEmpty = !orderedKeys.Any(k => k.StartsWith(key + "/"));
var isSelectable = _settingsTypeMap[key] is not null;
var parentKey = string.Join('/', key.Split('/').SkipLast(1));
var parent = string.IsNullOrEmpty(parentKey) ? _listElement : _listElement.Q(parentKey);
var foldout = new Foldout();
foldout.name = key;
foldout.text = key.Split('/').Last();
foldout.toggleOnLabelClick = false;
foldout.Q<Label>().RegisterCallback<MouseDownEvent>(_ => SelectKey(key));
if (isEmpty) foldout.AddToClassList("empty");
if (isSelectable) foldout.AddToClassList("selectable");
parent.Add(foldout);
}
}
private void SelectKey(string key)
{
var isSelectable = _settingsTypeMap[key] is not null;
if (!isSelectable) return;
_listElement.Query(className: "selected").ForEach(el => el.RemoveFromClassList("selected"));
_listElement.Q(key).AddToClassList("selected");
_currentKey = key;
RefreshInspector(key);
}
private void RefreshInspector(string key)
{
// Clicked foldout without settings attached - do nothing
if (_settingsTypeMap[key] is not {} settingsType) return;
var settings = _mainSettings.settings.FirstOrDefault(s => s.GetType() == settingsType);
_objectField.objectType = settingsType;
_objectField.SetValueWithoutNotify(settings);
_inspectorElement.Unbind();
_inspectorElement.Clear();
if (settings)
{
_inspectorElement.Bind(new SerializedObject(settings));
_missingElement.style.display = DisplayStyle.None;
}
else
{
_missingElement.style.display = DisplayStyle.Flex;
}
}
private void OnObjectChanged(Object newSettings)
{
hasUnsavedChanges = true;
var settingsType = _settingsTypeMap[_currentKey];
if (newSettings.GetType() != settingsType)
{
Debug.LogError($"Only acceptable type here is: <b>{settingsType}</b>");
return;
}
_mainSettings.settings.RemoveAll(s => s.GetType() == settingsType);
_mainSettings.settings.Add((CustomSettingsSO)newSettings);
RefreshInspector(_currentKey);
}
private void CreateMissingSettings(Type settingsType)
{
// Create and load wanted settings
if (!AssetDatabase.AssetPathExists(FolderPath)) AssetDatabase.CreateFolder("Assets", FolderName);
var path = FolderPath + "/" + settingsType.Name + ".asset";
AssetDatabase.CreateAsset(CreateInstance(settingsType), path);
var settings = AssetDatabase.LoadAssetAtPath<CustomSettingsSO>(path);
// Update Main Settings
_mainSettings.settings.RemoveAll(s => s.GetType() == settingsType);
_mainSettings.settings.Add(settings);
OnObjectChanged(settings);
}
private void CreateExampleSettings()
{
var absolutePath = Application.dataPath + "/CustomSettingsSO example.cs";
var relativePath = "Assets/CustomSettingsSO example.cs";
File.AppendAllText(absolutePath, CreateExampleContent);
AssetDatabase.ImportAsset(relativePath);
AssetDatabase.OpenAsset(AssetDatabase.LoadAssetAtPath<MonoScript>(relativePath));
}
private MainSettingsSO FindOrCreateMainSettings()
{
var mainPath = GetMainSettingsAssetPath();
var paths = AssetDatabase.FindAssets($"t:{nameof(MainSettingsSO)}").Select(AssetDatabase.GUIDToAssetPath).ToList();
paths.Remove(mainPath);
// MainSettings not found or moved - move to original folder or recreate if missing
if (!AssetDatabase.AssetPathExists(mainPath))
{
Debug.LogWarning($"{nameof(MainSettingsSO)} not found in '{mainPath}'!");
if (paths.Count > 0)
{
Debug.LogWarning($"{nameof(MainSettingsSO)} found in <b>{paths[0]}</b>, moved to its original path: <b>{mainPath}</b>!");
AssetDatabase.MoveAsset(paths[0], mainPath);
paths.RemoveAt(0);
}
else
{
AssetDatabase.CreateAsset(CreateInstance<MainSettingsSO>(), mainPath);
}
}
// Illegal or invalid assets found - DELETE them
if (paths.Count > 0)
{
Debug.LogWarning($"Multiple {nameof(MainSettingsSO)} found, deleting all excessive ones: {string.Join(", ", paths)}");
paths.ForEach(path => AssetDatabase.DeleteAsset(path));
}
return AssetDatabase.LoadAssetAtPath<MainSettingsSO>(mainPath);
}
private Dictionary<string, Type> CreateSettingsTypeMap()
{
var dict = new Dictionary<string, Type>();
// Assign unique key to each settings type
foreach (var settingsType in TypeCache.GetTypesDerivedFrom<CustomSettingsSO>())
{
if (!settingsType.IsClass || settingsType.IsAbstract) continue;
if (settingsType == typeof(MainSettingsSO)) continue;
var attribute = settingsType.GetCustomAttribute<CustomSettingsAttribute>();
var settingsPath = attribute?.path ?? settingsType.Name;
var pathDuplicated = false;
while (dict.ContainsKey(settingsPath))
{
pathDuplicated = true;
settingsPath += " ?";
}
if (pathDuplicated)
{
Debug.LogWarning($"Class <b>{settingsType}</b> has already used path, using <b>{settingsPath}</b> instead!" +
$"Change path with attribute <b>{nameof(CustomSettingsAttribute)}</b>");
}
dict[settingsPath] = settingsType;
}
// Fill empty key to create empty parents if needed
foreach (var key in dict.Keys.ToList())
{
var keySplitted = key.Split('/');
for (var i = 1; i < keySplitted.Length; i++)
dict.TryAdd(string.Join("/", keySplitted.Take(i)), null);
}
return dict;
}
private string GetMainSettingsAssetPath()
{
var projectPath = Application.dataPath.Replace("/Assets", "");
var filePath = CompilationPipeline.GetAssemblyDefinitionFilePathFromAssemblyName("RPGCoreCommon.Settings");
var path = Path.GetDirectoryName(filePath);
var relativePath = Path.GetRelativePath(projectPath, path);
var relativeAssetPath = Path.Combine(relativePath, "Resources", "MainSettings.asset");
return relativeAssetPath.Replace("\\", "/");
}
}
}