init
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
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("\\", "/");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user