This commit is contained in:
2026-04-25 23:37:10 +02:00
commit 19d6bd934a
476 changed files with 9198 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5d237fce79f744309f3744dcfb79d002
timeCreated: 1759058841
@@ -0,0 +1,21 @@
using UnityEditor;
using UnityEngine.UIElements;
namespace RPGCoreCommon.Settings.Editor
{
[CustomEditor(typeof(MainSettingsSO))]
internal class MainSettingsSODrawer : UnityEditor.Editor
{
public override VisualElement CreateInspectorGUI()
{
var rootElement = new VisualElement();
var button = new Button();
button.text = "Open settings editor";
button.clicked += SettingsEditorWindow.ShowSettingsEditor;
rootElement.Add(button);
return rootElement;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 55ff5e0dc4c245da9af8fa287298efaa
timeCreated: 1759062565
@@ -0,0 +1,19 @@
{
"name": "RPGCoreCommon.Settings.Editor",
"rootNamespace": "RPGCoreCommon.Settings.Editor",
"references": [
"RPGCoreCommon.Settings",
"RPGCoreCommon.Helpers"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 82c9ca36f8574ede9895a338716393c3
timeCreated: 1759058870
@@ -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("\\", "/");
}
}
}
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 81335a84d15c49adac057f7724847fb4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- m_ViewDataDictionary: {instanceID: 0}
- _visualTreeAsset: {fileID: 9197481963319205126, guid: 4de99465844afaa409a38eb7a9b3c599, type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,128 @@
#cs-left {
padding-right: 2px;
padding-left: 2px;
}
#cs-left #unity-content {
margin-top: 0;
margin-right: 0;
margin-bottom: 0;
margin-left: 0;
}
#cs-left Toggle {
margin-top: 2px;
margin-right: 2px;
margin-bottom: 2px;
margin-left: 2px;
padding-top: 1px;
padding-bottom: 1px;
}
#cs-left Foldout Toggle {
padding-left: 0;
}
#cs-left Foldout Foldout Toggle {
padding-left: 6px;
}
#cs-left Foldout Foldout Foldout Toggle {
padding-left: 12px;
}
#cs-left Foldout Foldout Foldout Foldout Toggle {
padding-left: 18px;
}
#cs-left Foldout Foldout Foldout Foldout Foldout Toggle {
padding-left: 24px;
}
#cs-left Foldout Foldout Foldout Foldout Foldout Foldout Toggle {
padding-left: 30px;
}
Foldout.selectable > Toggle:hover {
background-color: rgba(255, 255, 255, 0.04);
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
Foldout.selected > Toggle {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: rgba(255, 255, 255, 0.14);
-unity-font-style: bold;
}
Foldout.empty > Toggle #unity-checkmark {
visibility: hidden;
}
#cs-right {
}
#cs-right #cs-settings-object {
padding-top: 5px;
padding-right: 5px;
padding-bottom: 5px;
padding-left: 5px;
background-color: rgba(255, 255, 255, 0.12);
margin-top: 0;
margin-right: 0;
margin-bottom: 5px;
margin-left: 0;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
#cs-right #cs-settings-inspector #unity-input-m_Script {
display: none;
}
#cs-error {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
flex-direction: row;
flex-wrap: wrap;
background-color: var(--unity-colors-window-background);
display: none;
}
#cs-error > Label {
width: 100%;
font-size: 24px;
-unity-font-style: bold;
-unity-text-align: middle-center;
margin-top: 20px;
margin-bottom: 20px;
}
#cs-error > .cs-error-block {
width: 50%;
}
#cs-error > .cs-error-block > HelpBox {
flex-grow: 1;
}
#cs-error > .cs-error-block > Button {
}
#cs-missing {
display: none;
}
#cs-missing HelpBox {
flex-grow: 1;
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2328868bd9cdc2946a328e88f4b9f71c
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0
@@ -0,0 +1,45 @@
<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/RPGCoreCommon/Settings/Editor/SettingsEditorWindow.uss?fileID=7433441132597879392&amp;guid=2328868bd9cdc2946a328e88f4b9f71c&amp;type=3#SettingsEditorWindow" />
<RPGCoreCommon.Helpers.UIElements.TwoPaneSplitView fixed-pane-initial-dimension="160">
<ui:ScrollView name="cs-left">
<ui:Foldout text="Foldout" name="Foldout" class="selectable">
<ui:Foldout text="Foldout" class="empty selectable" />
<ui:Foldout text="Foldout" class="empty selectable" />
<ui:Foldout text="Foldout" class="selectable">
<ui:Foldout text="Foldout" class="empty selected selectable" />
<ui:Foldout text="Foldout" class="empty selectable" />
</ui:Foldout>
</ui:Foldout>
<ui:Foldout text="Foldout">
<ui:Foldout text="Foldout">
<ui:Foldout text="Foldout">
<ui:Foldout text="Foldout">
<ui:Foldout text="Foldout" class="selectable">
<ui:Foldout text="Foldout" name="Foldout" class="empty selectable" />
</ui:Foldout>
</ui:Foldout>
</ui:Foldout>
</ui:Foldout>
</ui:Foldout>
</ui:ScrollView>
<ui:VisualElement name="cs-right" style="flex-grow: 1;">
<uie:ObjectField label="Settings file:" name="cs-settings-object" type="RPGCoreCommon.Settings.CustomSettingsSO, RPGCoreCommon.Settings" />
<RPGCoreCommon.Helpers.Editor.UIElements.InspectorElement name="cs-settings-inspector" />
<ui:VisualElement name="cs-missing">
<RPGCoreCommon.Helpers.UIElements.HelpBox message-type="Warning" text="&lt;b&gt;CustomSettings is missing!&lt;/b&gt;&lt;br&gt;Drag/Select existing settings of that type or press &lt;b&gt;Generate...&lt;/b&gt; to create new one.&lt;br&gt;Generated file will be opened with default editor." />
<ui:Button text="Generate..." name="cs-create-missing" />
</ui:VisualElement>
</ui:VisualElement>
</RPGCoreCommon.Helpers.UIElements.TwoPaneSplitView>
<ui:VisualElement name="cs-error">
<ui:Label text="CustomSettings cannot be loaded!" />
<ui:VisualElement class="cs-error-block">
<RPGCoreCommon.Helpers.UIElements.HelpBox text="&lt;b&gt;Fix errors and reload domain&lt;/b&gt;&lt;br&gt;Its possible that unity didn&apos;t save your implementation of CustomSettingsSO to cache properly.&lt;br&gt;Fix all code errors and press &lt;b&gt;Reload Domain&lt;/b&gt; (ctrl + r)." message-type="Error" />
<ui:Button text="Reload Domain" name="cs-reload-domain" />
</ui:VisualElement>
<ui:VisualElement class="cs-error-block">
<RPGCoreCommon.Helpers.UIElements.HelpBox text="&lt;b&gt;Create new CustomSettingsSO example&lt;/b&gt;&lt;br&gt;You didn&apos;t create any CustomSettingsSO so far.&lt;br&gt;You can do it by yourself or by clicking &lt;b&gt;Create Example&lt;/b&gt;." message-type="Error" />
<ui:Button text="Create Example" name="cs-create-example" />
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 4de99465844afaa409a38eb7a9b3c599
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 998d4a8463fe4de69ae4b37b478e12df
timeCreated: 1759058847
@@ -0,0 +1,21 @@
using System;
using System.Text.RegularExpressions;
namespace RPGCoreCommon.Settings
{
[AttributeUsage(AttributeTargets.Class)]
public class CustomSettingsAttribute : Attribute
{
internal string path { private set; get; }
/// <summary>
/// Optionally attached to class that extends <see cref="CustomSettingsSO"/>.<br/>
/// All paths in project should be unique otherwise different name will be generated.
/// </summary>
/// <param name="path">custom path and name with "/" as delimiter.</param>
public CustomSettingsAttribute(string path)
{
this.path = new Regex("/+").Replace(path, "/").Trim('/');
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f707df1d72104aee928f9c94d1bdaf78
timeCreated: 1759151290
@@ -0,0 +1,13 @@
using System;
using UnityEngine;
namespace RPGCoreCommon.Settings
{
/// <summary>
/// Extend this to create new type of settings. These will be automatically added to settings editor window.
/// </summary>
[Serializable]
public abstract class CustomSettingsSO : ScriptableObject
{
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a0846fc0c7874dd182f4dd9f83a06acf
timeCreated: 1759059292
@@ -0,0 +1,8 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("RPGCoreCommon.Settings.Editor")]
namespace RPGCoreCommon.Settings
{
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6a05ba05117242eca64be63d17123d74
timeCreated: 1759063680
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using UnityEngine;
namespace RPGCoreCommon.Settings
{
internal class MainSettingsSO : CustomSettingsSO
{
[SerializeReference] internal List<CustomSettingsSO> settings;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e1b15ea3617b447290121c2f34009f42
timeCreated: 1759059460
@@ -0,0 +1,14 @@
{
"name": "RPGCoreCommon.Settings",
"rootNamespace": "RPGCoreCommon.Settings",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 42d6bbb0c4c8403d800c75cd28a1feef
timeCreated: 1759058936
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f81c1b5e2a3146898d759c18b6a9ffa0
timeCreated: 1759058859
@@ -0,0 +1,21 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e1b15ea3617b447290121c2f34009f42, type: 3}
m_Name: MainSettings
m_EditorClassIdentifier:
settings:
- {fileID: 11400000, guid: 6dbd2048bd875ca42bf5e2ccd3282671, type: 2}
- {fileID: 11400000, guid: 4bc4327b80b188146b7b4df067d792b0, type: 2}
- {fileID: 11400000, guid: f583031a590e02f4da97ee00f6d56be7, type: 2}
- {fileID: 11400000, guid: f96c774b23415ae46a33cd04b1eeb0f7, type: 2}
- {fileID: 11400000, guid: f1565c1ea85209c46b34b5066482be4b, type: 2}
- {fileID: 11400000, guid: 9df758ff4bf9af948aa6ad53f610288b, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0a07eaaeda7416b4f8c78d79c5f9fb8c
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
using System.Linq;
using UnityEngine;
namespace RPGCoreCommon.Settings
{
public static class SettingsManager
{
private static MainSettingsSO _mainSettings;
public static T Get<T>() where T : CustomSettingsSO
{
_mainSettings ??= Resources.Load<MainSettingsSO>("MainSettings");
return _mainSettings.settings.FirstOrDefault(settings => settings is T) as T;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9233d376eb8d441c8557a2489d3f39cd
timeCreated: 1759058999