278 lines
11 KiB
C#
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("\\", "/");
|
|
}
|
|
}
|
|
} |