From 8aca8efebbe39f575586e4e9663263f89be036cd Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Sat, 6 Jun 2026 22:26:04 +0300 Subject: [PATCH 01/18] feat(serialize-references): add [SerializeReferenceSelector] dropdown drawer Add a hierarchical type-selector dropdown for [SerializeReference] fields (single, array and List), reusing TypeSelectorWindow. Picking a concrete type instantiates it, clears the reference, the assigned instance's nested properties are drawn inline under a foldout, and an unresolved stored type is surfaced as a missing-type warning. Implemented for both IMGUI and UIToolkit inspectors. TypeSelectorWindow.Show gains an optional candidate-type filter (backward compatible) used to exclude UnityEngine.Object, open generics, strings and delegates. --- .../Documentation/EN/README.md | 56 ++++- .../Documentation/RU/README.md | 56 ++++- .../Resources/UI/SerializeReferences.meta | 8 + .../Aspid-FastTools-SerializeReference.uss | 44 ++++ ...spid-FastTools-SerializeReference.uss.meta | 12 ++ .../Editor/Scripts/SerializeReferences.meta | 8 + .../Scripts/SerializeReferences/Drawers.meta | 8 + .../SerializeReferenceIMGUIPropertyDrawer.cs | 150 +++++++++++++ ...ializeReferenceIMGUIPropertyDrawer.cs.meta | 11 + ...erializeReferenceSelectorPropertyDrawer.cs | 52 +++++ ...izeReferenceSelectorPropertyDrawer.cs.meta | 11 + ...rializeReferenceUIToolkitPropertyDrawer.cs | 15 ++ ...zeReferenceUIToolkitPropertyDrawer.cs.meta | 11 + .../SerializeReferences/Extensions.meta | 8 + .../Extensions/SerializeReferenceHelpers.cs | 84 ++++++++ .../SerializeReferenceHelpers.cs.meta | 11 + .../SerializeReferences/VisualElements.meta | 8 + .../VisualElements/SerializeReferenceField.cs | 202 ++++++++++++++++++ .../SerializeReferenceField.cs.meta | 11 + .../Types/Selectors/HierarchyBuilder.cs | 4 +- .../Scripts/Types/Selectors/TypeInfo.cs | 7 +- .../Types/Selectors/TypeSelectorWindow.cs | 14 +- .../Unity/Runtime/SerializeReferences.meta | 8 + .../SerializeReferenceSelectorAttribute.cs | 40 ++++ ...erializeReferenceSelectorAttribute.cs.meta | 11 + CHANGELOG.md | 4 + README.md | 56 ++++- README_RU.md | 56 ++++- 28 files changed, 952 insertions(+), 14 deletions(-) create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceSelectorPropertyDrawer.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceSelectorPropertyDrawer.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceUIToolkitPropertyDrawer.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceUIToolkitPropertyDrawer.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences/SerializeReferenceSelectorAttribute.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences/SerializeReferenceSelectorAttribute.cs.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/EN/README.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/EN/README.md index 5b241f97..9e045b50 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/EN/README.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/EN/README.md @@ -17,6 +17,7 @@ - **Features** - [ProfilerMarker](#profilermarker) - [Serializable Type System](#serializable-type-system) + - [SerializeReference Selector](#serializereference-selector) - [Enum System](#enum-system) - [ID System (Beta)](#id-system-beta) - [SerializedProperty Extensions](#serializedproperty-extensions) @@ -273,7 +274,8 @@ namespace Aspid.FastTools.Types.Editors Type[] types = null, string currentAqn = "", TypeAllow allow = TypeAllow.None, - Action onSelected = null); + Action onSelected = null, + Func filter = null); } } ``` @@ -285,6 +287,7 @@ namespace Aspid.FastTools.Types.Editors | `currentAqn` | Assembly-qualified name of the currently selected type, used to pre-navigate to its location. Pass `null` or empty to start at the root. | | `allow` | Which special type kinds (abstract classes, interfaces) are included in addition to concrete classes. Default: `TypeAllow.None`. | | `onSelected` | Callback invoked with the assembly-qualified name of the selected type, or `null` if the user chose ``. | +| `filter` | Optional predicate applied to each candidate type after the base-type and `allow` checks. Return `false` to hide a type. Pass `null` to keep every match. | ### ComponentTypeSelector @@ -325,6 +328,57 @@ public sealed class TankEnemy : EnemyBase --- +## SerializeReference Selector + +A drop-in dropdown for `[SerializeReference]` fields. Add `[SerializeReferenceSelector]` next to `[SerializeReference]` and the Inspector replaces the default managed-reference UI with the same searchable, hierarchical type picker used by `SerializableType` — letting you choose which concrete implementation of the field's declared type is instantiated. + +- Lists every concrete, non-`UnityEngine.Object` class assignable to the field's declared interface / base type. +- Picking a type instantiates it; `` clears the reference. +- The assigned instance's serialized fields are drawn inline under a foldout. +- A stored type that no longer resolves (renamed or deleted) is surfaced as a missing-type warning instead of silently clearing. +- Works on single fields, arrays, and `List`, in both IMGUI and UIToolkit inspectors. + +```csharp +using System; +using UnityEngine; +using System.Collections.Generic; +using Aspid.FastTools.SerializeReferences; + +public interface IWeapon +{ + void Fire(); +} + +[Serializable] +public sealed class Pistol : IWeapon +{ + [SerializeField] [Min(0)] private int _damage = 10; + + public void Fire() => Debug.Log($"Pistol: {_damage} dmg"); +} + +[Serializable] +public sealed class Railgun : IWeapon +{ + [SerializeField] [Min(0)] private float _chargeTime = 1.5f; + + public void Fire() => Debug.Log($"Railgun charged for {_chargeTime}s"); +} + +public sealed class Loadout : MonoBehaviour +{ + [SerializeReference] [SerializeReferenceSelector] + private IWeapon _primary; + + [SerializeReference] [SerializeReferenceSelector] + private List _sidearms; +} +``` + +The attribute is editor-only (`[Conditional("UNITY_EDITOR")]`) and carries no runtime cost. + +--- + ## Enum System Provides serializable enum-to-value mappings configurable from the Inspector. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/RU/README.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/RU/README.md index 54ad4964..52108e98 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/RU/README.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/RU/README.md @@ -17,6 +17,7 @@ - **Features** - [ProfilerMarker](#profilermarker) - [Serializable Type System](#serializable-type-system) + - [SerializeReference Selector](#serializereference-selector) - [Enum System](#enum-system) - [ID System (Beta)](#id-system-beta) - [SerializedProperty Extensions](#serializedproperty-extensions) @@ -273,7 +274,8 @@ namespace Aspid.FastTools.Types.Editors Type[] types = null, string currentAqn = "", TypeAllow allow = TypeAllow.None, - Action onSelected = null); + Action onSelected = null, + Func filter = null); } } ``` @@ -285,6 +287,7 @@ namespace Aspid.FastTools.Types.Editors | `currentAqn` | Assembly-qualified имя текущего выбранного типа: окно сразу откроется на его уровне иерархии. Передайте `null` или пустую строку, чтобы стартовать с корня. | | `allow` | Какие специальные категории (абстрактные классы, интерфейсы) включаются в список в дополнение к конкретным классам. По умолчанию: `TypeAllow.None`. | | `onSelected` | Callback с assembly-qualified именем выбранного типа или `null`, если пользователь выбрал ``. | +| `filter` | Необязательный предикат, применяемый к каждому типу-кандидату после проверок базового типа и `allow`. Верните `false`, чтобы скрыть тип. Передайте `null`, чтобы оставить все совпадения. | ### ComponentTypeSelector @@ -325,6 +328,57 @@ public sealed class TankEnemy : EnemyBase --- +## SerializeReference Selector + +Готовый выпадающий список для полей с `[SerializeReference]`. Добавьте `[SerializeReferenceSelector]` рядом с `[SerializeReference]`, и Inspector заменит стандартный UI managed-ссылки тем же иерархическим выбором типа с поиском, что используется в `SerializableType` — позволяя выбрать, какая конкретная реализация объявленного типа поля будет создана. + +- Показывает каждый конкретный, не наследующий `UnityEngine.Object` класс, совместимый с объявленным интерфейсом / базовым типом поля. +- Выбор типа создаёт его экземпляр; `` очищает ссылку. +- Сериализуемые поля назначенного экземпляра рисуются вложенно под foldout. +- Сохранённый тип, который больше не разрешается (переименован или удалён), показывается как предупреждение о потерянном типе, а не очищается молча. +- Работает с одиночными полями, массивами и `List`, в инспекторах IMGUI и UIToolkit. + +```csharp +using System; +using UnityEngine; +using System.Collections.Generic; +using Aspid.FastTools.SerializeReferences; + +public interface IWeapon +{ + void Fire(); +} + +[Serializable] +public sealed class Pistol : IWeapon +{ + [SerializeField] [Min(0)] private int _damage = 10; + + public void Fire() => Debug.Log($"Pistol: {_damage} dmg"); +} + +[Serializable] +public sealed class Railgun : IWeapon +{ + [SerializeField] [Min(0)] private float _chargeTime = 1.5f; + + public void Fire() => Debug.Log($"Railgun charged for {_chargeTime}s"); +} + +public sealed class Loadout : MonoBehaviour +{ + [SerializeReference] [SerializeReferenceSelector] + private IWeapon _primary; + + [SerializeReference] [SerializeReferenceSelector] + private List _sidearms; +} +``` + +Атрибут существует только в редакторе (`[Conditional("UNITY_EDITOR")]`) и не несёт стоимости в рантайме. + +--- + ## Enum System Предоставляет сериализуемые отображения enum → значение, настраиваемые через Inspector. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences.meta new file mode 100644 index 00000000..fe9608c5 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 31a60714a36b461385fa435ea880b724 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss new file mode 100644 index 00000000..a5f30507 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss @@ -0,0 +1,44 @@ +:root { + margin: 0; +} + +/* Lay the foldout header out as a row so the type dropdown sits in the value column. */ +.aspid-fasttools-serialize-reference .unity-foldout__toggle { + flex-direction: row; +} + +.aspid-fasttools-serialize-reference .unity-foldout__toggle > .unity-toggle__input { + flex-grow: 0; + flex-shrink: 0; +} + +/* The type dropdown fills the remaining width of the header row. */ +.aspid-fasttools-serialize-reference__dropdown { + flex-grow: 1; + margin: 0 0 0 2px; +} + +/* Hide the expand arrow when the reference is empty — there is nothing to expand. */ +.aspid-fasttools-serialize-reference--empty .unity-foldout__toggle .unity-toggle__checkmark { + visibility: hidden; +} + +/* Open-script button mirrors the SerializableType drawer affordance. */ +.aspid-fasttools-serialize-reference Button { + padding: 0; + min-width: 18px; + max-width: 18px; + min-height: 18px; + max-height: 18px; + margin: 0 0 0 1px; +} + +.aspid-fasttools-serialize-reference Button > VisualElement { + width: 100%; + height: 100%; + background-image: resource("d_Folder Icon"); +} + +.aspid-fasttools-serialize-reference Button > VisualElement:enabled:hover { + background-image: resource("d_FolderOpened Icon"); +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss.meta new file mode 100644 index 00000000..261dd60c --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Resources/UI/SerializeReferences/Aspid-FastTools-SerializeReference.uss.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 80c610f029234224a2ab1568059370a3 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 + unsupportedSelectorAction: 0 diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences.meta new file mode 100644 index 00000000..e3cfd0d3 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ee1385cfb3fb4d79b8831e0de0102dcf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers.meta new file mode 100644 index 00000000..dac523a3 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4765d7eeba4748d3acd9142e2d2dca03 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs new file mode 100644 index 00000000..bb25276c --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs @@ -0,0 +1,150 @@ +using System; +using UnityEditor; +using UnityEngine; +using Aspid.FastTools.Types; +using Aspid.FastTools.Editors; +using Aspid.FastTools.Types.Editors; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.SerializeReferences.Editors +{ + /// + /// IMGUI rendering for the [SerializeReferenceSelector] drawer: a foldout-and-dropdown header + /// row, an optional missing-type warning, and the nested properties of the assigned instance. + /// + internal static class SerializeReferenceIMGUIPropertyDrawer + { + public static float GetHeight(SerializedProperty property) + { + var spacing = EditorGUIUtility.standardVerticalSpacing; + var height = EditorGUIUtility.singleLineHeight; + + if (SerializeReferenceHelpers.IsMissingType(property)) + height += spacing + GetWarningHeight(); + + if (property.managedReferenceValue is not null && property.isExpanded) + height += GetChildrenHeight(property, spacing); + + return height; + } + + public static void Draw(Rect position, GUIContent label, SerializedProperty property, params Type[] types) + { + var spacing = EditorGUIUtility.standardVerticalSpacing; + var currentType = SerializeReferenceHelpers.GetCurrentType(property); + var hasValue = currentType is not null; + + var line = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); + + var labelRect = new Rect(line.x, line.y, EditorGUIUtility.labelWidth, line.height); + if (hasValue) property.isExpanded = EditorGUI.Foldout(labelRect, property.isExpanded, label, toggleOnLabelClick: true); + else EditorGUI.LabelField(labelRect, label); + + var dropdownRect = new Rect( + line.x + EditorGUIUtility.labelWidth + 2f, + line.y, + line.width - EditorGUIUtility.labelWidth - 2f, + line.height); + + var openRect = Rect.zero; + if (hasValue) + { + var openSize = line.height; + openRect = new Rect(dropdownRect.xMax - openSize, dropdownRect.y, openSize, openSize); + dropdownRect.width -= openSize + 1f; + } + + var caption = GetCaption(property, currentType); + if (EditorGUI.DropdownButton(dropdownRect, new GUIContent(caption), FocusType.Passive)) + ShowSelector(property, types, currentType, dropdownRect); + + if (hasValue) + TypeIMGUIPropertyDrawer.DrawOpenScriptButton(openRect, currentType); + + var y = line.yMax + spacing; + + if (SerializeReferenceHelpers.IsMissingType(property)) + { + var warningHeight = GetWarningHeight(); + var warningRect = new Rect(position.x, y, position.width, warningHeight); + EditorGUI.HelpBox(warningRect, $"Missing type: {property.managedReferenceFullTypename}", MessageType.Warning); + y += warningHeight + spacing; + } + + if (!hasValue || !property.isExpanded) return; + + EditorGUI.indentLevel++; + DrawChildren(property, position.x, position.width, spacing, ref y); + EditorGUI.indentLevel--; + } + + private static void DrawChildren(SerializedProperty property, float x, float width, float spacing, ref float y) + { + var iterator = property.Copy(); + var end = property.GetEndProperty(); + var enterChildren = true; + + while (iterator.NextVisible(enterChildren) && !SerializedProperty.EqualContents(iterator, end)) + { + enterChildren = false; + + var height = EditorGUI.GetPropertyHeight(iterator, includeChildren: true); + EditorGUI.PropertyField(new Rect(x, y, width, height), iterator, includeChildren: true); + y += height + spacing; + } + } + + private static float GetChildrenHeight(SerializedProperty property, float spacing) + { + var height = 0f; + var iterator = property.Copy(); + var end = property.GetEndProperty(); + var enterChildren = true; + + while (iterator.NextVisible(enterChildren) && !SerializedProperty.EqualContents(iterator, end)) + { + enterChildren = false; + height += EditorGUI.GetPropertyHeight(iterator, includeChildren: true) + spacing; + } + + return height; + } + + private static void ShowSelector(SerializedProperty property, Type[] types, Type currentType, Rect dropdownRect) + { + var persistent = property.Persistent(); + var screenPosition = GUIUtility.GUIToScreenPoint(new Vector2(dropdownRect.x, dropdownRect.y)); + var screenRect = new Rect(screenPosition.x, screenPosition.y, dropdownRect.width, dropdownRect.height); + + TypeSelectorWindow.Show( + screenRect: screenRect, + types: types, + currentAqn: currentType?.AssemblyQualifiedName ?? string.Empty, + allow: TypeAllow.None, + onSelected: assemblyQualifiedName => + { + var selectedType = string.IsNullOrEmpty(assemblyQualifiedName) + ? null + : Type.GetType(assemblyQualifiedName, throwOnError: false); + + persistent.SetManagedReferenceAndApply(SerializeReferenceHelpers.CreateInstance(selectedType)); + persistent.isExpanded = selectedType is not null; + }, + filter: SerializeReferenceHelpers.IsAssignableManagedReference); + } + + private static string GetCaption(SerializedProperty property, Type currentType) + { + if (currentType is not null) + return TypeSelectorHelpers.GetTypeSelectorTitle(currentType); + + var missingName = SerializeReferenceHelpers.IsMissingType(property) + ? property.managedReferenceFullTypename + : null; + + return TypeSelectorHelpers.GetTypeSelectorTitle(null, missingName); + } + + private static float GetWarningHeight() => EditorGUIUtility.singleLineHeight * 2f; + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs.meta new file mode 100644 index 00000000..0473bc6d --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c73abd645ce47b8b2d711c39909eaa8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceSelectorPropertyDrawer.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceSelectorPropertyDrawer.cs new file mode 100644 index 00000000..e1468f20 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceSelectorPropertyDrawer.cs @@ -0,0 +1,52 @@ +using System; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.SerializeReferences.Editors +{ + /// + /// Property drawer for . Delegates rendering to the + /// IMGUI and UIToolkit helpers, constraining the candidate list to the field's declared managed-reference type. + /// + [CustomPropertyDrawer(typeof(SerializeReferenceSelectorAttribute))] + internal sealed class SerializeReferenceSelectorPropertyDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + ThrowExceptionIfInvalidProperty(property); + + SerializeReferenceIMGUIPropertyDrawer.Draw( + position: position, + label: label, + property: property, + types: SerializeReferenceHelpers.GetFieldType(property)); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + if (property.propertyType != SerializedPropertyType.ManagedReference) + return EditorGUIUtility.singleLineHeight; + + return SerializeReferenceIMGUIPropertyDrawer.GetHeight(property); + } + + public override VisualElement CreatePropertyGUI(SerializedProperty property) + { + ThrowExceptionIfInvalidProperty(property); + + return SerializeReferenceUIToolkitPropertyDrawer.Draw( + label: preferredLabel, + property: property); + } + + private static void ThrowExceptionIfInvalidProperty(SerializedProperty property) + { + if (property.propertyType != SerializedPropertyType.ManagedReference) + throw new ArgumentException( + "[SerializeReferenceSelector] can only be applied to a [SerializeReference] field.", + nameof(property)); + } + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceSelectorPropertyDrawer.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceSelectorPropertyDrawer.cs.meta new file mode 100644 index 00000000..d586cb43 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceSelectorPropertyDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 928e59b0c8ab460cade339ce4e824b17 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceUIToolkitPropertyDrawer.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceUIToolkitPropertyDrawer.cs new file mode 100644 index 00000000..a0929ca6 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceUIToolkitPropertyDrawer.cs @@ -0,0 +1,15 @@ +using UnityEditor; +using UnityEngine.UIElements; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.SerializeReferences.Editors +{ + internal static class SerializeReferenceUIToolkitPropertyDrawer + { + public static VisualElement Draw(string label, SerializedProperty property) + { + label = string.IsNullOrWhiteSpace(label) ? null : label; + return new SerializeReferenceField(label, property); + } + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceUIToolkitPropertyDrawer.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceUIToolkitPropertyDrawer.cs.meta new file mode 100644 index 00000000..2bfcbff4 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceUIToolkitPropertyDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7cbfa5b504864624a596b92a76a09ec1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions.meta new file mode 100644 index 00000000..672e73b8 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4b6da85b53a545ed8bb4d2d6bed835b7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs new file mode 100644 index 00000000..8b7b1f5b --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs @@ -0,0 +1,84 @@ +using System; +using UnityEditor; +using System.Runtime.Serialization; +using Object = UnityEngine.Object; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.SerializeReferences.Editors +{ + /// + /// Shared helpers for the [SerializeReferenceSelector] drawers: resolving the declared + /// managed-reference field type, filtering candidate types, instantiating the selected type, and + /// parsing Unity's managed-reference type-name format. + /// + internal static class SerializeReferenceHelpers + { + /// + /// Resolves the declared element type of a managed-reference property — the base type that + /// constrains the candidate list. Uses , + /// which already reports the element type for array/list entries. + /// + public static Type GetFieldType(SerializedProperty property) => + GetTypeFromTypename(property.managedReferenceFieldTypename) ?? typeof(object); + + /// + /// Resolves the concrete type currently stored in the managed reference, or + /// when the reference is empty or its stored type can no longer be loaded. + /// + public static Type GetCurrentType(SerializedProperty property) => + property.managedReferenceValue?.GetType(); + + /// + /// Returns when the property stores a type name that no longer resolves to a + /// loadable type (a renamed or deleted implementation), so the value reads back as . + /// + public static bool IsMissingType(SerializedProperty property) => + property.managedReferenceValue is null && + !string.IsNullOrEmpty(property.managedReferenceFullTypename); + + /// + /// Predicate identifying types that can legally be assigned to a [SerializeReference] field: + /// concrete reference types that are neither , open generics, strings, nor delegates. + /// + public static bool IsAssignableManagedReference(Type type) => + type is { IsClass: true, IsAbstract: false, ContainsGenericParameters: false } && + type != typeof(string) && + !typeof(Object).IsAssignableFrom(type) && + !typeof(Delegate).IsAssignableFrom(type); + + /// + /// Creates an instance of for assignment to a managed reference. + /// Prefers a (public or non-public) parameterless constructor so field initializers run, and + /// falls back to an uninitialized instance for types that expose no parameterless constructor. + /// + public static object CreateInstance(Type type) + { + if (type is null) return null; + + try + { + return Activator.CreateInstance(type, nonPublic: true); + } + catch (MissingMethodException) + { + return FormatterServices.GetUninitializedObject(type); + } + } + + /// + /// Parses Unity's managed-reference type-name format ("AssemblyName Namespace.TypeName") + /// into a , or when it is empty or cannot be loaded. + /// + public static Type GetTypeFromTypename(string typename) + { + if (string.IsNullOrEmpty(typename)) return null; + + var separator = typename.IndexOf(' '); + if (separator < 0) return Type.GetType(typename, throwOnError: false); + + var assembly = typename[..separator]; + var fullName = typename[(separator + 1)..]; + return Type.GetType($"{fullName}, {assembly}", throwOnError: false); + } + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs.meta new file mode 100644 index 00000000..a5d12ec0 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 81c62c61701e4f6d88b678ddae8380ba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements.meta new file mode 100644 index 00000000..9a07bca8 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 70b1c58023de4500a33542b7e00b8346 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs new file mode 100644 index 00000000..9292952d --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs @@ -0,0 +1,202 @@ +using System; +using UnityEditor; +using UnityEngine.UIElements; +using UnityEditor.UIElements; +using Aspid.FastTools.Types; +using Aspid.FastTools.Editors; +using Aspid.FastTools.UIElements; +using Aspid.FastTools.Types.Editors; +using Aspid.FastTools.UIElements.Editors.Internal; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.SerializeReferences.Editors +{ + /// + /// UIToolkit field for a [SerializeReference] property: a foldout whose header carries an + /// EnumField-style type dropdown (backed by ) and an open-script button, + /// whose content hosts the assigned instance's nested properties, and which surfaces a missing-type + /// warning when the stored type can no longer be resolved. + /// + /// + /// Always bound to a managed-reference ; created by the + /// , not from UXML. The field keeps the live + /// inspector property so child fields round-trip through Unity's binding (apply/Undo) and only rebuilds + /// the nested properties when the assigned type actually changes. + /// + internal sealed class SerializeReferenceField : VisualElement + { + private const string StyleSheetPath = "UI/SerializeReferences/Aspid-FastTools-SerializeReference"; + + private const string BlockClass = "aspid-fasttools-serialize-reference"; + private const string EmptyClass = BlockClass + "--empty"; + private const string DropdownClass = BlockClass + "__dropdown"; + + private readonly Foldout _foldout; + private readonly TextElement _caption; + private readonly VisualElement _dropdown; + private readonly Button _openButton; + private readonly VisualElement _content; + private readonly SerializedProperty _property; + private readonly Type[] _types; + + private AspidHelpBox _missingBox; + private Type _currentType; + private bool _contentBuilt; + + public SerializeReferenceField(string label, SerializedProperty property) + { + _property = property; + _types = new[] { SerializeReferenceHelpers.GetFieldType(_property) }; + + this.AddClass(BlockClass) + .AddClass(PropertyField.ussClassName) + .AddStyleSheetsFromResource(StyleSheetPath) + .AddStyleSheetsFromResource(AspidStyles.DefaultStyleSheet); + + _foldout = new Foldout().SetText(label); + _foldout.RegisterValueChangedCallback(OnFoldoutToggled); + _content = _foldout.contentContainer; + + _caption = new TextElement() + .AddClass(EnumField.textUssClassName) + .SetPickingMode(PickingMode.Ignore); + + _dropdown = new VisualElement() + .AddClass(EnumField.ussClassName) + .AddClass(EnumField.inputUssClassName) + .AddClass(DropdownClass) + .AddChild(_caption) + .AddChild(new VisualElement() + .AddClass(EnumField.arrowUssClassName) + .SetPickingMode(PickingMode.Ignore)); + + _dropdown.RegisterCallback(OnDropdownClicked); + + _openButton = new Button() + .AddChild(new VisualElement()) + .AddClicked(() => SerializeReferenceHelpers.GetCurrentType(_property)?.OpenInScriptEditor()); + + _foldout.Q() + .AddChild(_dropdown) + .AddChild(_openButton); + + this.AddChild(_foldout); + + Refresh(forceRebuild: true); + this.TrackPropertyValue(_property, _ => Refresh(forceRebuild: false)); + } + + private void Refresh(bool forceRebuild) + { + var currentType = SerializeReferenceHelpers.GetCurrentType(_property); + var hasValue = currentType is not null; + + _caption.SetText(GetCaption(currentType)); + _openButton.SetDisplay(hasValue ? DisplayStyle.Flex : DisplayStyle.None); + + EnableInClassList(EmptyClass, !hasValue); + _foldout.SetValueWithoutNotify(hasValue && _property.isExpanded); + + UpdateMissingBox(); + + if (forceRebuild || !_contentBuilt || currentType != _currentType) + { + _currentType = currentType; + RebuildContent(hasValue); + } + } + + private void RebuildContent(bool hasValue) + { + _content.Clear(); + _contentBuilt = true; + if (!hasValue) return; + + var iterator = _property.Copy(); + var end = _property.GetEndProperty(); + var enterChildren = true; + + while (iterator.NextVisible(enterChildren) && !SerializedProperty.EqualContents(iterator, end)) + { + enterChildren = false; + + var child = iterator.Copy(); + var field = new PropertyField(child); + field.BindProperty(child); + + _content.Add(field); + } + } + + private void UpdateMissingBox() + { + if (!SerializeReferenceHelpers.IsMissingType(_property)) + { + _missingBox?.RemoveFromHierarchy(); + return; + } + + _missingBox ??= new AspidHelpBox(AspidHelpBoxPreset.Default.SetMessageType(HelpBoxMessageType.Warning)); + _missingBox.Message = $"Missing type: {_property.managedReferenceFullTypename}"; + + if (_missingBox.parent is null) this.AddChild(_missingBox); + } + + private void OnFoldoutToggled(ChangeEvent evt) + { + if (evt.target != _foldout) return; + _property.isExpanded = evt.newValue; + } + + private void OnDropdownClicked(PointerDownEvent evt) + { + if (evt.button is not 0) return; + + var window = EditorWindow.focusedWindow != null + ? EditorWindow.focusedWindow + : EditorWindow.mouseOverWindow; + + if (!window) return; + + var currentType = SerializeReferenceHelpers.GetCurrentType(_property); + + TypeSelectorWindow.Show( + screenRect: GetScreenRect(), + types: _types, + currentAqn: currentType?.AssemblyQualifiedName ?? string.Empty, + allow: TypeAllow.None, + onSelected: assemblyQualifiedName => + { + var selectedType = string.IsNullOrEmpty(assemblyQualifiedName) + ? null + : Type.GetType(assemblyQualifiedName, throwOnError: false); + + _property.SetManagedReferenceAndApply(SerializeReferenceHelpers.CreateInstance(selectedType)); + _property.isExpanded = selectedType is not null; + Refresh(forceRebuild: true); + }, + filter: SerializeReferenceHelpers.IsAssignableManagedReference); + + evt.StopPropagation(); + return; + + Rect GetScreenRect() => new( + window.position.x + _dropdown.worldBound.xMin, + window.position.y + _dropdown.worldBound.yMin, + _dropdown.worldBound.width, + _dropdown.worldBound.height); + } + + private string GetCaption(Type currentType) + { + if (currentType is not null) + return TypeSelectorHelpers.GetTypeSelectorTitle(currentType); + + var missingName = SerializeReferenceHelpers.IsMissingType(_property) + ? _property.managedReferenceFullTypename + : null; + + return TypeSelectorHelpers.GetTypeSelectorTitle(null, missingName); + } + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs.meta new file mode 100644 index 00000000..5d74f4b5 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4fb75ace46bd4133b8821cabec35b4c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs index d74335b6..ac3de995 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs @@ -7,9 +7,9 @@ namespace Aspid.FastTools.Types.Editors { internal static class HierarchyBuilder { - public static TreeNode Build(Type[] types, TypeAllow allow) + public static TreeNode Build(Type[] types, TypeAllow allow, Func filter = null) { - var allTypes = TypeInfo.GetAllTypeInfos(types, allow); + var allTypes = TypeInfo.GetAllTypeInfos(types, allow, filter); var root = new TreeNode("/"); root.Children.Add(new TreeNode(TypeSelectorHelpers.NoneOption, null, TypeSelectorHelpers.NoneOption)); diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs index c4353e09..b77232b8 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs @@ -25,14 +25,14 @@ public TypeInfo(Type type) Namespace = string.IsNullOrEmpty(type.Namespace) ? TypeSelectorHelpers.GlobalNamespace : type.Namespace; } - public static List GetAllTypeInfos(Type[] baseTypes, TypeAllow allow) + public static List GetAllTypeInfos(Type[] baseTypes, TypeAllow allow, Func filter = null) { var result = new List(); foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { Type[] types; - + try { types = assembly.GetTypes(); @@ -49,7 +49,8 @@ public static List GetAllTypeInfos(Type[] baseTypes, TypeAllow allow) !t.Name.Contains("<") && !t.Name.Contains(">") && (allow.HasFlag(TypeAllow.Abstract) || !t.IsAbstract) && - (allow.HasFlag(TypeAllow.Interface) || !t.IsInterface)) + (allow.HasFlag(TypeAllow.Interface) || !t.IsInterface) && + (filter is null || filter(t))) .Select(type => new TypeInfo(type))); } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs index c5b132d0..c10e31b2 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs @@ -38,22 +38,25 @@ public sealed class TypeSelectorWindow : EditorWindow /// Assembly-qualified name of the currently selected type, used to pre-navigate to that type's location. Pass null or empty to start at the root. /// Which type kinds are included in the list. Defaults to TypeAllow.None. /// Callback invoked with the assembly-qualified name of the selected type, or null if the user chose <None>. + /// Optional predicate applied to each candidate type after the base-type and checks. Return false to hide a type. Pass null to keep every matching type. public static void Show( Rect screenRect, Type[] types = null, string currentAqn = "", TypeAllow allow = TypeAllow.None, - Action onSelected = null) + Action onSelected = null, + Func filter = null) { types ??= new[] { typeof(object) }; - + var window = CreateInstance(); window.Initialize( screenRect, types, currentAqn, allow, - onSelected); + onSelected, + filter); } #region Initialization @@ -62,14 +65,15 @@ private void Initialize( Type[] types, string currentAqn, TypeAllow allow, - Action onSelected) + Action onSelected, + Func filter) { _onSelected = onSelected; _currentAqn = currentAqn ?? string.Empty; BuildUI(); - var hierarchy = HierarchyBuilder.Build(types, allow); + var hierarchy = HierarchyBuilder.Build(types, allow, filter); InitializeNavigation(hierarchy, _currentAqn); RefreshView(); diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences.meta new file mode 100644 index 00000000..e410998c --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1c91b2eb3b6e4914a11516da4082e932 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences/SerializeReferenceSelectorAttribute.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences/SerializeReferenceSelectorAttribute.cs new file mode 100644 index 00000000..6c29061d --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences/SerializeReferenceSelectorAttribute.cs @@ -0,0 +1,40 @@ +using UnityEngine; +using System.Diagnostics; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.SerializeReferences +{ + /// + /// Replaces the default [SerializeReference] inspector with a hierarchical type-selector + /// dropdown, letting the user pick which concrete implementation of the field's declared type + /// is instantiated and assigned to the managed reference. + /// + /// + /// + /// Apply this attribute together with [SerializeReference]. The declared type of the + /// field (interface, abstract class, or base class) defines the set of candidate types; the dropdown + /// lists every concrete, non- class assignable to it. Picking a type creates a + /// fresh instance, <None> clears the reference, and a type whose serialized data no longer + /// resolves is surfaced as a missing-reference warning. + /// + /// + /// Works on plain fields, arrays, and of a + /// [SerializeReference] type. Only compiled in editor assemblies (UNITY_EDITOR). + /// + /// + /// + /// Single polymorphic field: + /// + /// [SerializeReference, SerializeReferenceSelector] + /// private IWeapon _weapon; + /// + /// + /// List of polymorphic elements: + /// + /// [SerializeReference, SerializeReferenceSelector] + /// private List<IWeapon> _weapons; + /// + /// + [Conditional(conditionString: "UNITY_EDITOR")] + public sealed class SerializeReferenceSelectorAttribute : PropertyAttribute { } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences/SerializeReferenceSelectorAttribute.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences/SerializeReferenceSelectorAttribute.cs.meta new file mode 100644 index 00000000..a3636a5b --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Runtime/SerializeReferences/SerializeReferenceSelectorAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 06b0bfc795364795ad3f5e106256f97b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CHANGELOG.md b/CHANGELOG.md index f9a622bb..9b1c3658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- `[SerializeReferenceSelector]` attribute and property drawer — a hierarchical type-selector dropdown for `[SerializeReference]` fields (and arrays / `List` of them). Picking a concrete implementation instantiates it, `` clears the reference, the assigned instance's nested properties are drawn inline under a foldout, and a stored type that no longer resolves is surfaced as a missing-type warning. Works in both IMGUI and UIToolkit inspectors and reuses the existing `TypeSelectorWindow`. +- `TypeSelectorWindow.Show` gained an optional `filter` predicate that further narrows the candidate list after the base-type and `TypeAllow` checks (used by the SerializeReference drawer to exclude `UnityEngine.Object`, open generics, strings and delegates). + ## [1.0.0-rc.5] — 2026-06-06 Packaging-only release. No functional or API changes versus `1.0.0-rc.4`. diff --git a/README.md b/README.md index cc11a37b..45d57f32 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - **Features** - [ProfilerMarker](#profilermarker) - [Serializable Type System](#serializable-type-system) + - [SerializeReference Selector](#serializereference-selector) - [Enum System](#enum-system) - [ID System (Beta)](#id-system-beta) - [SerializedProperty Extensions](#serializedproperty-extensions) @@ -276,7 +277,8 @@ namespace Aspid.FastTools.Types.Editors Type[] types = null, string currentAqn = "", TypeAllow allow = TypeAllow.None, - Action onSelected = null); + Action onSelected = null, + Func filter = null); } } ``` @@ -288,6 +290,7 @@ namespace Aspid.FastTools.Types.Editors | `currentAqn` | Assembly-qualified name of the currently selected type, used to pre-navigate to its location. Pass `null` or empty to start at the root. | | `allow` | Which special type kinds (abstract classes, interfaces) are included in addition to concrete classes. Default: `TypeAllow.None`. | | `onSelected` | Callback invoked with the assembly-qualified name of the selected type, or `null` if the user chose ``. | +| `filter` | Optional predicate applied to each candidate type after the base-type and `allow` checks. Return `false` to hide a type. Pass `null` to keep every match. | ### ComponentTypeSelector @@ -328,6 +331,57 @@ public sealed class TankEnemy : EnemyBase --- +## SerializeReference Selector + +A drop-in dropdown for `[SerializeReference]` fields. Add `[SerializeReferenceSelector]` next to `[SerializeReference]` and the Inspector replaces the default managed-reference UI with the same searchable, hierarchical type picker used by `SerializableType` — letting you choose which concrete implementation of the field's declared type is instantiated. + +- Lists every concrete, non-`UnityEngine.Object` class assignable to the field's declared interface / base type. +- Picking a type instantiates it; `` clears the reference. +- The assigned instance's serialized fields are drawn inline under a foldout. +- A stored type that no longer resolves (renamed or deleted) is surfaced as a missing-type warning instead of silently clearing. +- Works on single fields, arrays, and `List`, in both IMGUI and UIToolkit inspectors. + +```csharp +using System; +using UnityEngine; +using System.Collections.Generic; +using Aspid.FastTools.SerializeReferences; + +public interface IWeapon +{ + void Fire(); +} + +[Serializable] +public sealed class Pistol : IWeapon +{ + [SerializeField] [Min(0)] private int _damage = 10; + + public void Fire() => Debug.Log($"Pistol: {_damage} dmg"); +} + +[Serializable] +public sealed class Railgun : IWeapon +{ + [SerializeField] [Min(0)] private float _chargeTime = 1.5f; + + public void Fire() => Debug.Log($"Railgun charged for {_chargeTime}s"); +} + +public sealed class Loadout : MonoBehaviour +{ + [SerializeReference] [SerializeReferenceSelector] + private IWeapon _primary; + + [SerializeReference] [SerializeReferenceSelector] + private List _sidearms; +} +``` + +The attribute is editor-only (`[Conditional("UNITY_EDITOR")]`) and carries no runtime cost. + +--- + ## Enum System Provides serializable enum-to-value mappings configurable from the Inspector. diff --git a/README_RU.md b/README_RU.md index 9d3e2751..aee6cc9f 100644 --- a/README_RU.md +++ b/README_RU.md @@ -20,6 +20,7 @@ - **Features** - [ProfilerMarker](#profilermarker) - [Serializable Type System](#serializable-type-system) + - [SerializeReference Selector](#serializereference-selector) - [Enum System](#enum-system) - [ID System (Beta)](#id-system-beta) - [SerializedProperty Extensions](#serializedproperty-extensions) @@ -276,7 +277,8 @@ namespace Aspid.FastTools.Types.Editors Type[] types = null, string currentAqn = "", TypeAllow allow = TypeAllow.None, - Action onSelected = null); + Action onSelected = null, + Func filter = null); } } ``` @@ -288,6 +290,7 @@ namespace Aspid.FastTools.Types.Editors | `currentAqn` | Assembly-qualified имя текущего выбранного типа: окно сразу откроется на его уровне иерархии. Передайте `null` или пустую строку, чтобы стартовать с корня. | | `allow` | Какие специальные категории (абстрактные классы, интерфейсы) включаются в список в дополнение к конкретным классам. По умолчанию: `TypeAllow.None`. | | `onSelected` | Callback с assembly-qualified именем выбранного типа или `null`, если пользователь выбрал ``. | +| `filter` | Необязательный предикат, применяемый к каждому типу-кандидату после проверок базового типа и `allow`. Верните `false`, чтобы скрыть тип. Передайте `null`, чтобы оставить все совпадения. | ### ComponentTypeSelector @@ -328,6 +331,57 @@ public sealed class TankEnemy : EnemyBase --- +## SerializeReference Selector + +Готовый выпадающий список для полей с `[SerializeReference]`. Добавьте `[SerializeReferenceSelector]` рядом с `[SerializeReference]`, и Inspector заменит стандартный UI managed-ссылки тем же иерархическим выбором типа с поиском, что используется в `SerializableType` — позволяя выбрать, какая конкретная реализация объявленного типа поля будет создана. + +- Показывает каждый конкретный, не наследующий `UnityEngine.Object` класс, совместимый с объявленным интерфейсом / базовым типом поля. +- Выбор типа создаёт его экземпляр; `` очищает ссылку. +- Сериализуемые поля назначенного экземпляра рисуются вложенно под foldout. +- Сохранённый тип, который больше не разрешается (переименован или удалён), показывается как предупреждение о потерянном типе, а не очищается молча. +- Работает с одиночными полями, массивами и `List`, в инспекторах IMGUI и UIToolkit. + +```csharp +using System; +using UnityEngine; +using System.Collections.Generic; +using Aspid.FastTools.SerializeReferences; + +public interface IWeapon +{ + void Fire(); +} + +[Serializable] +public sealed class Pistol : IWeapon +{ + [SerializeField] [Min(0)] private int _damage = 10; + + public void Fire() => Debug.Log($"Pistol: {_damage} dmg"); +} + +[Serializable] +public sealed class Railgun : IWeapon +{ + [SerializeField] [Min(0)] private float _chargeTime = 1.5f; + + public void Fire() => Debug.Log($"Railgun charged for {_chargeTime}s"); +} + +public sealed class Loadout : MonoBehaviour +{ + [SerializeReference] [SerializeReferenceSelector] + private IWeapon _primary; + + [SerializeReference] [SerializeReferenceSelector] + private List _sidearms; +} +``` + +Атрибут существует только в редакторе (`[Conditional("UNITY_EDITOR")]`) и не несёт стоимости в рантайме. + +--- + ## Enum System Предоставляет сериализуемые отображения enum → значение, настраиваемые через Inspector. From 10581a06a7f52206087487d306fbd495a5b1e78c Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Sat, 6 Jun 2026 22:47:05 +0300 Subject: [PATCH 02/18] fix(serialize-references): import UnityEngine for Rect in SerializeReferenceField --- .../VisualElements/SerializeReferenceField.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs index 9292952d..68104a40 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs @@ -1,5 +1,6 @@ using System; using UnityEditor; +using UnityEngine; using UnityEngine.UIElements; using UnityEditor.UIElements; using Aspid.FastTools.Types; From 0840d23c713e428934806431a5b152905f481b66 Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Sat, 6 Jun 2026 22:47:05 +0300 Subject: [PATCH 03/18] docs(serialize-references): add SerializeReferences sample Add a loadout-system sample exercising [SerializeReferenceSelector]: single IWeapon field, List, abstract StatusEffect base, and a nested [SerializeReference] inside Railgun. Includes an IMGUILoadout + forcing editor to demonstrate the IMGUI path, EN/RU README, and a package.json sample entry. --- .../Samples~/SerializeReferences.meta | 3 ++ .../Samples~/SerializeReferences/README.md | 24 ++++++++++ .../SerializeReferences/README.md.meta | 7 +++ .../Samples~/SerializeReferences/README_RU.md | 24 ++++++++++ .../SerializeReferences/README_RU.md.meta | 7 +++ .../Samples~/SerializeReferences/Scripts.meta | 3 ++ ...stTools.Samples.SerializeReferences.asmdef | 16 +++++++ ...ls.Samples.SerializeReferences.asmdef.meta | 7 +++ .../SerializeReferences/Scripts/Editor.meta | 3 ++ ....Samples.SerializeReferences.Editor.asmdef | 18 ++++++++ ...les.SerializeReferences.Editor.asmdef.meta | 7 +++ .../Scripts/Editor/IMGUILoadoutEditor.cs | 22 ++++++++++ .../Scripts/Editor/IMGUILoadoutEditor.cs.meta | 11 +++++ .../SerializeReferences/Scripts/Effects.meta | 3 ++ .../Scripts/Effects/BurnEffect.cs | 14 ++++++ .../Scripts/Effects/BurnEffect.cs.meta | 11 +++++ .../Scripts/Effects/FreezeEffect.cs | 14 ++++++ .../Scripts/Effects/FreezeEffect.cs.meta | 11 +++++ .../Scripts/Effects/StatusEffect.cs | 21 +++++++++ .../Scripts/Effects/StatusEffect.cs.meta | 11 +++++ .../Scripts/IMGUILoadout.cs | 25 +++++++++++ .../Scripts/IMGUILoadout.cs.meta | 11 +++++ .../SerializeReferences/Scripts/Loadout.cs | 44 +++++++++++++++++++ .../Scripts/Loadout.cs.meta | 11 +++++ .../SerializeReferences/Scripts/Weapons.meta | 3 ++ .../Scripts/Weapons/IWeapon.cs | 12 +++++ .../Scripts/Weapons/IWeapon.cs.meta | 11 +++++ .../Scripts/Weapons/Pistol.cs | 17 +++++++ .../Scripts/Weapons/Pistol.cs.meta | 11 +++++ .../Scripts/Weapons/Railgun.cs | 24 ++++++++++ .../Scripts/Weapons/Railgun.cs.meta | 11 +++++ .../Scripts/Weapons/Shotgun.cs | 15 +++++++ .../Scripts/Weapons/Shotgun.cs.meta | 11 +++++ .../tech.aspid.fasttools/package.json | 5 +++ 34 files changed, 448 insertions(+) create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Aspid.FastTools.Samples.SerializeReferences.asmdef create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Aspid.FastTools.Samples.SerializeReferences.asmdef.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/Aspid.FastTools.Samples.SerializeReferences.Editor.asmdef create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/Aspid.FastTools.Samples.SerializeReferences.Editor.asmdef.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/IMGUILoadoutEditor.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/IMGUILoadoutEditor.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/BurnEffect.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/BurnEffect.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/FreezeEffect.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/FreezeEffect.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/StatusEffect.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/StatusEffect.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/IWeapon.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/IWeapon.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Pistol.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Pistol.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Railgun.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Railgun.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Shotgun.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Shotgun.cs.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences.meta new file mode 100644 index 00000000..9d6b8820 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5dca98ae8db24794bbb8b57f9074d562 +timeCreated: 1780775167 diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md new file mode 100644 index 00000000..b099ae83 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md @@ -0,0 +1,24 @@ +# SerializeReferences Sample + +A tiny loadout system that demonstrates `[SerializeReferenceSelector]` — a searchable, hierarchical type dropdown for `[SerializeReference]` fields. You pick which concrete implementation of a polymorphic field is instantiated, directly in the Inspector. + +Look at: + +- `Scripts/Loadout.cs` — single (`IWeapon`), `List`, and abstract-base (`StatusEffect`) `[SerializeReference]` fields, each annotated with `[SerializeReferenceSelector]`. +- `Scripts/Weapons/` — `IWeapon` interface and its implementations (`Pistol`, `Shotgun`, `Railgun`). `Railgun` nests another `[SerializeReferenceSelector]` field, showing recursive polymorphic editing. +- `Scripts/Effects/` — abstract `StatusEffect` base with `BurnEffect` / `FreezeEffect`. The dropdown offers only the concrete subclasses; the abstract base is never listed. + +The drawer ships both a UIToolkit and an IMGUI rendering path. The `IMGUILoadout` variant forces the IMGUI path so you can compare them or migrate IMGUI-only projects: + +- `Scripts/IMGUILoadout.cs` + `Scripts/Editor/IMGUILoadoutEditor.cs` — the same fields rendered via `OnInspectorGUI` (`SerializeReferenceIMGUIPropertyDrawer`). + +## How to run + +1. Create an empty GameObject in any scene and add the **Loadout** component (UIToolkit path) or **IMGUILoadout** component (IMGUI path). +2. In the Inspector, click a `` dropdown and pick an implementation — e.g. `Primary Weapon → Railgun`. The instance is created and its serialized fields appear inline under the foldout. +3. Pick `Railgun`'s nested `Charge Effect → BurnEffect` to see recursive polymorphic editing. +4. Press **+** on `Sidearms` and give each element its own weapon type. +5. Set `On Hit Effect` — note only `BurnEffect` / `FreezeEffect` are offered (the abstract `StatusEffect` is hidden). +6. Right-click the component header → **Log Loadout** to print the configured loadout to the Console. + +Switching a field back to `` clears the reference. If a stored type is later renamed or deleted, the dropdown shows a `` caption and a warning instead of silently clearing. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md.meta new file mode 100644 index 00000000..e0f714a1 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 28ef597d95ad4acda660bc86c164648b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md new file mode 100644 index 00000000..3c7a855c --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md @@ -0,0 +1,24 @@ +# Пример SerializeReferences + +Маленькая система снаряжения, демонстрирующая `[SerializeReferenceSelector]` — иерархический выпадающий список с поиском для полей `[SerializeReference]`. Вы прямо в Inspector выбираете, какая конкретная реализация полиморфного поля будет создана. + +Смотрите: + +- `Scripts/Loadout.cs` — одиночное поле (`IWeapon`), `List` и поле с абстрактным базовым типом (`StatusEffect`), каждое с `[SerializeReference]` и `[SerializeReferenceSelector]`. +- `Scripts/Weapons/` — интерфейс `IWeapon` и его реализации (`Pistol`, `Shotgun`, `Railgun`). `Railgun` вкладывает ещё одно поле `[SerializeReferenceSelector]` — показывает рекурсивное полиморфное редактирование. +- `Scripts/Effects/` — абстрактный базовый `StatusEffect` с `BurnEffect` / `FreezeEffect`. В списке предлагаются только конкретные подтипы; абстрактный базовый класс никогда не показывается. + +Drawer поддерживает и UIToolkit, и IMGUI. Вариант `IMGUILoadout` принудительно использует IMGUI-путь — удобно для сравнения или миграции IMGUI-проектов: + +- `Scripts/IMGUILoadout.cs` + `Scripts/Editor/IMGUILoadoutEditor.cs` — те же поля, отрисованные через `OnInspectorGUI` (`SerializeReferenceIMGUIPropertyDrawer`). + +## Как запустить + +1. Создайте пустой GameObject в любой сцене и добавьте компонент **Loadout** (путь UIToolkit) или **IMGUILoadout** (путь IMGUI). +2. В Inspector кликните по выпадающему списку `` и выберите реализацию — например, `Primary Weapon → Railgun`. Экземпляр создастся, а его сериализуемые поля появятся вложенно под foldout. +3. Выберите вложенный у `Railgun` `Charge Effect → BurnEffect` — увидите рекурсивное полиморфное редактирование. +4. Нажмите **+** на `Sidearms` и задайте каждому элементу свой тип оружия. +5. Задайте `On Hit Effect` — обратите внимание, что предлагаются только `BurnEffect` / `FreezeEffect` (абстрактный `StatusEffect` скрыт). +6. ПКМ по заголовку компонента → **Log Loadout**, чтобы вывести настроенное снаряжение в Console. + +Переключение поля обратно на `` очищает ссылку. Если сохранённый тип позже переименуют или удалят, в списке появится подпись `` и предупреждение, вместо тихой очистки. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md.meta new file mode 100644 index 00000000..64992c02 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2ed2b6607ff347f6ae540ace02dbc14c +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts.meta new file mode 100644 index 00000000..d3d21526 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0c6859af7d0f4560a27fc9398c16cac2 +timeCreated: 1780775167 diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Aspid.FastTools.Samples.SerializeReferences.asmdef b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Aspid.FastTools.Samples.SerializeReferences.asmdef new file mode 100644 index 00000000..6aeeefc0 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Aspid.FastTools.Samples.SerializeReferences.asmdef @@ -0,0 +1,16 @@ +{ + "name": "Aspid.FastTools.Samples.SerializeReferences", + "rootNamespace": "", + "references": [ + "Aspid.FastTools.Unity" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Aspid.FastTools.Samples.SerializeReferences.asmdef.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Aspid.FastTools.Samples.SerializeReferences.asmdef.meta new file mode 100644 index 00000000..2202ab8f --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Aspid.FastTools.Samples.SerializeReferences.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9fad5519270642308b608484e669eeaf +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor.meta new file mode 100644 index 00000000..29604cb6 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5ae54c703d8f4ad4a074367f0614d47c +timeCreated: 1780775167 diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/Aspid.FastTools.Samples.SerializeReferences.Editor.asmdef b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/Aspid.FastTools.Samples.SerializeReferences.Editor.asmdef new file mode 100644 index 00000000..672a61e1 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/Aspid.FastTools.Samples.SerializeReferences.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "Aspid.FastTools.Samples.SerializeReferences.Editor", + "rootNamespace": "", + "references": [ + "Aspid.FastTools.Samples.SerializeReferences" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/Aspid.FastTools.Samples.SerializeReferences.Editor.asmdef.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/Aspid.FastTools.Samples.SerializeReferences.Editor.asmdef.meta new file mode 100644 index 00000000..721adc5e --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/Aspid.FastTools.Samples.SerializeReferences.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 97608434520c4d078f28ab868e7f3e06 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/IMGUILoadoutEditor.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/IMGUILoadoutEditor.cs new file mode 100644 index 00000000..299dbb4c --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/IMGUILoadoutEditor.cs @@ -0,0 +1,22 @@ +using UnityEditor; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences.Editors +{ + // Forces IMGUI rendering for the IMGUILoadout inspector. + // + // Unity picks IMGUI vs UIToolkit at the Editor level: when CreateInspectorGUI is NOT + // overridden but OnInspectorGUI is, the whole inspector — including every nested + // PropertyDrawer — falls back to IMGUI. That routes [SerializeReferenceSelector] fields + // through SerializeReferenceIMGUIPropertyDrawer.OnGUI instead of CreatePropertyGUI. + [CustomEditor(typeof(IMGUILoadout))] + internal sealed class IMGUILoadoutEditor : Editor + { + public override void OnInspectorGUI() + { + serializedObject.Update(); + DrawPropertiesExcluding(serializedObject, "m_Script"); + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/IMGUILoadoutEditor.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/IMGUILoadoutEditor.cs.meta new file mode 100644 index 00000000..0d589592 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Editor/IMGUILoadoutEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 49097153234d4b66b7eacaff0ba4d88d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects.meta new file mode 100644 index 00000000..2efe7f26 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0f9e11c2bbf345a0a2895fc744afba61 +timeCreated: 1780775167 diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/BurnEffect.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/BurnEffect.cs new file mode 100644 index 00000000..24dc603f --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/BurnEffect.cs @@ -0,0 +1,14 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + [Serializable] + public sealed class BurnEffect : StatusEffect + { + [SerializeField] [Min(0f)] private float _damagePerSecond = 5f; + + public override string Describe() => $"Burn — {_damagePerSecond} dps for {Duration}s"; + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/BurnEffect.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/BurnEffect.cs.meta new file mode 100644 index 00000000..1c4aba5a --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/BurnEffect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7dd6bde21faf4019b74c28d087f07570 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/FreezeEffect.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/FreezeEffect.cs new file mode 100644 index 00000000..c5e91e6f --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/FreezeEffect.cs @@ -0,0 +1,14 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + [Serializable] + public sealed class FreezeEffect : StatusEffect + { + [SerializeField] [Range(0f, 100f)] private float _slowPercent = 40f; + + public override string Describe() => $"Freeze — {_slowPercent}% slow for {Duration}s"; + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/FreezeEffect.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/FreezeEffect.cs.meta new file mode 100644 index 00000000..f5bdb149 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/FreezeEffect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 95af46da00c94957a8642c716ab6aabb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/StatusEffect.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/StatusEffect.cs new file mode 100644 index 00000000..465d1a6a --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/StatusEffect.cs @@ -0,0 +1,21 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + // Abstract base for a second polymorphic hierarchy. + // + // When a field is declared as StatusEffect, [SerializeReferenceSelector] offers only the + // concrete subclasses (BurnEffect, FreezeEffect) — the abstract base itself is never listed, + // because it cannot be instantiated. + [Serializable] + public abstract class StatusEffect + { + [SerializeField] [Min(0f)] private float _duration = 3f; + + protected float Duration => _duration; + + public abstract string Describe(); + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/StatusEffect.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/StatusEffect.cs.meta new file mode 100644 index 00000000..2574018b --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Effects/StatusEffect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aea8b9d6094b41bf80cab9036bf88bbe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs new file mode 100644 index 00000000..ab12c6fb --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using UnityEngine; +using Aspid.FastTools.SerializeReferences; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + // Same fields as Loadout, but the companion editor (IMGUILoadoutEditor) overrides + // OnInspectorGUI without CreateInspectorGUI, forcing the entire inspector — and every + // nested [SerializeReferenceSelector] field — through the IMGUI path + // (SerializeReferenceIMGUIPropertyDrawer) instead of the UIToolkit one. + // + // Use this to verify both rendering paths stay visually and behaviourally aligned. + public sealed class IMGUILoadout : MonoBehaviour + { + [SerializeReference] [SerializeReferenceSelector] + private IWeapon _primaryWeapon; + + [SerializeReference] [SerializeReferenceSelector] + private List _sidearms = new(); + + [SerializeReference] [SerializeReferenceSelector] + private StatusEffect _onHitEffect; + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs.meta new file mode 100644 index 00000000..ba18fad5 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e6cd8f6abf02422a92c5f183b53afa29 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs new file mode 100644 index 00000000..728b4069 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using UnityEngine; +using Aspid.FastTools.SerializeReferences; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + // Demonstrates [SerializeReferenceSelector] through the default (UIToolkit) Inspector. + // + // Add [SerializeReferenceSelector] next to [SerializeReference] and the field renders as a + // searchable, hierarchical type dropdown: + // - single field → pick one IWeapon implementation + // - List / array → each element is its own polymorphic picker + // - abstract base type → only concrete subclasses are offered + // + // Picking a type instantiates it, clears the reference, and the assigned instance's + // serialized fields appear inline under the foldout. Nested [SerializeReference] fields + // (e.g. Railgun's charge effect) get their own dropdown recursively. + public sealed class Loadout : MonoBehaviour + { + // Interface-typed field: lists every IWeapon implementation (Pistol, Shotgun, Railgun). + [SerializeReference] [SerializeReferenceSelector] + private IWeapon _primaryWeapon; + + // Each list element is its own independent picker. + [SerializeReference] [SerializeReferenceSelector] + private List _sidearms = new(); + + // Abstract-base field: the picker offers BurnEffect / FreezeEffect, never StatusEffect. + [SerializeReference] [SerializeReferenceSelector] + private StatusEffect _onHitEffect; + + [ContextMenu("Log Loadout")] + private void LogLoadout() + { + Debug.Log($"Primary: {_primaryWeapon?.Describe() ?? "none"}"); + + for (var i = 0; i < _sidearms.Count; i++) + Debug.Log($"Sidearm {i}: {_sidearms[i]?.Describe() ?? "none"}"); + + Debug.Log($"On-hit effect: {_onHitEffect?.Describe() ?? "none"}"); + } + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs.meta new file mode 100644 index 00000000..00265ea2 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 884d53b5154744d3af6948b1eef02505 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons.meta new file mode 100644 index 00000000..0e76adff --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b9b0ae5ee78a4faa9a79767c9f22771b +timeCreated: 1780775167 diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/IWeapon.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/IWeapon.cs new file mode 100644 index 00000000..14f1039f --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/IWeapon.cs @@ -0,0 +1,12 @@ +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + // Base interface for the polymorphic [SerializeReference] sample. + // + // [SerializeReferenceSelector] lists every concrete, non-UnityEngine.Object class + // assignable to the field's declared type — here, every IWeapon implementation. + public interface IWeapon + { + string Describe(); + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/IWeapon.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/IWeapon.cs.meta new file mode 100644 index 00000000..b7e5c0aa --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/IWeapon.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4559c2a45a7844d28bee4f6935696eea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Pistol.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Pistol.cs new file mode 100644 index 00000000..fec54fdd --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Pistol.cs @@ -0,0 +1,17 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + // Concrete IWeapon. Its serialized fields are drawn inline under the dropdown's foldout + // once it is assigned. [Serializable] is conventional for managed-reference payloads. + [Serializable] + public sealed class Pistol : IWeapon + { + [SerializeField] [Min(0)] private int _damage = 10; + [SerializeField] [Min(0)] private int _magazineSize = 12; + + public string Describe() => $"Pistol — {_damage} dmg, {_magazineSize}-round mag"; + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Pistol.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Pistol.cs.meta new file mode 100644 index 00000000..cce63b4b --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Pistol.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9acccfdd901f4e38b86499e3577cf2b4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Railgun.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Railgun.cs new file mode 100644 index 00000000..db4bc8b4 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Railgun.cs @@ -0,0 +1,24 @@ +using System; +using UnityEngine; +using Aspid.FastTools.SerializeReferences; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + // Demonstrates a nested [SerializeReference] inside a managed-reference payload: + // the charge effect is itself polymorphic and gets its own inline dropdown. + [Serializable] + public sealed class Railgun : IWeapon + { + [SerializeField] [Min(0f)] private float _chargeTime = 1.5f; + + [SerializeReference] [SerializeReferenceSelector] + private StatusEffect _chargeEffect; + + public string Describe() + { + var effect = _chargeEffect is null ? "none" : _chargeEffect.Describe(); + return $"Railgun — {_chargeTime}s charge, effect: {effect}"; + } + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Railgun.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Railgun.cs.meta new file mode 100644 index 00000000..c81685d5 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Railgun.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bf44f2297b1c4276a53ffe31f331254e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Shotgun.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Shotgun.cs new file mode 100644 index 00000000..c93cb855 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Shotgun.cs @@ -0,0 +1,15 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + [Serializable] + public sealed class Shotgun : IWeapon + { + [SerializeField] [Min(1)] private int _pellets = 8; + [SerializeField] [Range(0f, 90f)] private float _spreadAngle = 25f; + + public string Describe() => $"Shotgun — {_pellets} pellets, {_spreadAngle}° spread"; + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Shotgun.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Shotgun.cs.meta new file mode 100644 index 00000000..97569737 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Weapons/Shotgun.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6b6a4b841bbf4765a143eaa74ca1d6a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/package.json b/Aspid.FastTools/Packages/tech.aspid.fasttools/package.json index 87eed15a..850cd3b9 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/package.json +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/package.json @@ -38,6 +38,11 @@ "description": "SerializableType, SerializableType, TypeSelectorAttribute and ComponentTypeSelector usage with the editor type picker.", "path": "Samples~/Types" }, + { + "displayName": "SerializeReferences", + "description": "[SerializeReferenceSelector] type-picker dropdown for [SerializeReference] fields (single, List, abstract base, nested), in both UIToolkit and IMGUI inspectors.", + "path": "Samples~/SerializeReferences" + }, { "displayName": "EnumValues", "description": "Inspector-serializable enum-keyed dictionary, including [Flags] handling.", From 05ce9e6d8c9eb6099bab4bce7045fa044071a136 Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Sat, 6 Jun 2026 23:07:38 +0300 Subject: [PATCH 04/18] docs(serialize-references): add prefabs to SerializeReferences sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Loadout.prefab (UIToolkit) and IMGUILoadout.prefab (IMGUI) with pre-filled managed references — single (Railgun + nested BurnEffect), List ([Pistol, Shotgun]) and abstract-base (FreezeEffect / BurnEffect) — so the sample can be inspected without building it by hand. Update the EN/RU sample README "How to run" to drive the prefabs. --- .../Samples~/SerializeReferences/Prefabs.meta | 8 ++ .../Prefabs/IMGUILoadout.prefab | 64 +++++++++++++++ .../Prefabs/IMGUILoadout.prefab.meta | 7 ++ .../Prefabs/Loadout.prefab | 82 +++++++++++++++++++ .../Prefabs/Loadout.prefab.meta | 7 ++ .../Samples~/SerializeReferences/README.md | 20 +++-- .../Samples~/SerializeReferences/README_RU.md | 20 +++-- 7 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/IMGUILoadout.prefab create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/IMGUILoadout.prefab.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/Loadout.prefab create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/Loadout.prefab.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs.meta new file mode 100644 index 00000000..edaf3f9d --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cc14437c0afa46b8843c58f14cdbb07c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/IMGUILoadout.prefab b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/IMGUILoadout.prefab new file mode 100644 index 00000000..f81eedb1 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/IMGUILoadout.prefab @@ -0,0 +1,64 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &6600000000000000001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6600000000000000002} + - component: {fileID: 6600000000000000003} + m_Layer: 0 + m_Name: IMGUILoadout + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &6600000000000000002 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6600000000000000001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &6600000000000000003 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6600000000000000001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e6cd8f6abf02422a92c5f183b53afa29, type: 3} + m_Name: + m_EditorClassIdentifier: Aspid.FastTools.Samples.SerializeReferences::Aspid.FastTools.Samples.SerializeReferences.IMGUILoadout + _primaryWeapon: + rid: 2001 + _sidearms: [] + _onHitEffect: + rid: 2002 + references: + version: 2 + RefIds: + - rid: 2001 + type: {class: Pistol, ns: Aspid.FastTools.Samples.SerializeReferences, asm: Aspid.FastTools.Samples.SerializeReferences} + data: + _damage: 10 + _magazineSize: 12 + - rid: 2002 + type: {class: BurnEffect, ns: Aspid.FastTools.Samples.SerializeReferences, asm: Aspid.FastTools.Samples.SerializeReferences} + data: + _duration: 3 + _damagePerSecond: 5 diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/IMGUILoadout.prefab.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/IMGUILoadout.prefab.meta new file mode 100644 index 00000000..c71e4a92 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/IMGUILoadout.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 78c0fa006a244ee78b5a2dfd9c6618a6 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/Loadout.prefab b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/Loadout.prefab new file mode 100644 index 00000000..3ff1a95b --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/Loadout.prefab @@ -0,0 +1,82 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &6500000000000000001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6500000000000000002} + - component: {fileID: 6500000000000000003} + m_Layer: 0 + m_Name: Loadout + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &6500000000000000002 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6500000000000000001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &6500000000000000003 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6500000000000000001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 884d53b5154744d3af6948b1eef02505, type: 3} + m_Name: + m_EditorClassIdentifier: Aspid.FastTools.Samples.SerializeReferences::Aspid.FastTools.Samples.SerializeReferences.Loadout + _primaryWeapon: + rid: 1001 + _sidearms: + - rid: 1002 + - rid: 1003 + _onHitEffect: + rid: 1004 + references: + version: 2 + RefIds: + - rid: 1001 + type: {class: Railgun, ns: Aspid.FastTools.Samples.SerializeReferences, asm: Aspid.FastTools.Samples.SerializeReferences} + data: + _chargeTime: 2 + _chargeEffect: + rid: 1005 + - rid: 1002 + type: {class: Pistol, ns: Aspid.FastTools.Samples.SerializeReferences, asm: Aspid.FastTools.Samples.SerializeReferences} + data: + _damage: 15 + _magazineSize: 12 + - rid: 1003 + type: {class: Shotgun, ns: Aspid.FastTools.Samples.SerializeReferences, asm: Aspid.FastTools.Samples.SerializeReferences} + data: + _pellets: 8 + _spreadAngle: 25 + - rid: 1004 + type: {class: FreezeEffect, ns: Aspid.FastTools.Samples.SerializeReferences, asm: Aspid.FastTools.Samples.SerializeReferences} + data: + _duration: 2.5 + _slowPercent: 40 + - rid: 1005 + type: {class: BurnEffect, ns: Aspid.FastTools.Samples.SerializeReferences, asm: Aspid.FastTools.Samples.SerializeReferences} + data: + _duration: 3 + _damagePerSecond: 5 diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/Loadout.prefab.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/Loadout.prefab.meta new file mode 100644 index 00000000..7b7dd168 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Prefabs/Loadout.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 90d26bbfdf2e48fba5c42d214308e9fd +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md index b099ae83..311d2165 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md @@ -14,11 +14,19 @@ The drawer ships both a UIToolkit and an IMGUI rendering path. The `IMGUILoadout ## How to run -1. Create an empty GameObject in any scene and add the **Loadout** component (UIToolkit path) or **IMGUILoadout** component (IMGUI path). -2. In the Inspector, click a `` dropdown and pick an implementation — e.g. `Primary Weapon → Railgun`. The instance is created and its serialized fields appear inline under the foldout. -3. Pick `Railgun`'s nested `Charge Effect → BurnEffect` to see recursive polymorphic editing. -4. Press **+** on `Sidearms` and give each element its own weapon type. -5. Set `On Hit Effect` — note only `BurnEffect` / `FreezeEffect` are offered (the abstract `StatusEffect` is hidden). -6. Right-click the component header → **Log Loadout** to print the configured loadout to the Console. +Two ready-made prefabs live in `Prefabs/` — double-click to open in Prefab Mode, or drag either into any scene: + +- **Loadout** (`Prefabs/Loadout.prefab`) — UIToolkit path. Pre-filled: `Primary Weapon = Railgun` (with a nested `BurnEffect` charge effect), `Sidearms = [Pistol, Shotgun]`, `On Hit Effect = FreezeEffect`. +- **IMGUILoadout** (`Prefabs/IMGUILoadout.prefab`) — IMGUI path. Pre-filled: `Primary Weapon = Pistol`, `On Hit Effect = BurnEffect`. + +Then experiment with the dropdowns: + +1. Click any type dropdown and pick another implementation — the instance is created and its serialized fields appear inline under the foldout. +2. Expand `Railgun` and change its nested `Charge Effect` to see recursive polymorphic editing. +3. Press **+** on `Sidearms` and give each element its own weapon type. +4. Open `On Hit Effect` — note only `BurnEffect` / `FreezeEffect` are offered (the abstract `StatusEffect` is hidden). +5. Right-click the component header → **Log Loadout** to print the configured loadout to the Console. + +Prefer building from scratch? Add an empty GameObject and attach the **Loadout** (UIToolkit) or **IMGUILoadout** (IMGUI) component. Switching a field back to `` clears the reference. If a stored type is later renamed or deleted, the dropdown shows a `` caption and a warning instead of silently clearing. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md index 3c7a855c..449c0751 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md @@ -14,11 +14,19 @@ Drawer поддерживает и UIToolkit, и IMGUI. Вариант `IMGUILoa ## Как запустить -1. Создайте пустой GameObject в любой сцене и добавьте компонент **Loadout** (путь UIToolkit) или **IMGUILoadout** (путь IMGUI). -2. В Inspector кликните по выпадающему списку `` и выберите реализацию — например, `Primary Weapon → Railgun`. Экземпляр создастся, а его сериализуемые поля появятся вложенно под foldout. -3. Выберите вложенный у `Railgun` `Charge Effect → BurnEffect` — увидите рекурсивное полиморфное редактирование. -4. Нажмите **+** на `Sidearms` и задайте каждому элементу свой тип оружия. -5. Задайте `On Hit Effect` — обратите внимание, что предлагаются только `BurnEffect` / `FreezeEffect` (абстрактный `StatusEffect` скрыт). -6. ПКМ по заголовку компонента → **Log Loadout**, чтобы вывести настроенное снаряжение в Console. +В `Prefabs/` лежат два готовых префаба — дважды кликните, чтобы открыть в Prefab Mode, или перетащите любой в сцену: + +- **Loadout** (`Prefabs/Loadout.prefab`) — путь UIToolkit. Предзаполнено: `Primary Weapon = Railgun` (с вложенным эффектом заряда `BurnEffect`), `Sidearms = [Pistol, Shotgun]`, `On Hit Effect = FreezeEffect`. +- **IMGUILoadout** (`Prefabs/IMGUILoadout.prefab`) — путь IMGUI. Предзаполнено: `Primary Weapon = Pistol`, `On Hit Effect = BurnEffect`. + +Дальше поэкспериментируйте с выпадающими списками: + +1. Кликните по любому дропдауну типа и выберите другую реализацию — экземпляр создастся, а его сериализуемые поля появятся вложенно под foldout. +2. Разверните `Railgun` и смените вложенный `Charge Effect` — увидите рекурсивное полиморфное редактирование. +3. Нажмите **+** на `Sidearms` и задайте каждому элементу свой тип оружия. +4. Откройте `On Hit Effect` — обратите внимание, что предлагаются только `BurnEffect` / `FreezeEffect` (абстрактный `StatusEffect` скрыт). +5. ПКМ по заголовку компонента → **Log Loadout**, чтобы вывести настроенное снаряжение в Console. + +Хотите собрать с нуля? Добавьте пустой GameObject и прикрепите компонент **Loadout** (UIToolkit) или **IMGUILoadout** (IMGUI). Переключение поля обратно на `` очищает ссылку. Если сохранённый тип позже переименуют или удалят, в списке появится подпись `` и предупреждение, вместо тихой очистки. From f463b7f10d23a4c3aae96db06489dac1a9d7e150 Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Sun, 7 Jun 2026 16:39:12 +0300 Subject: [PATCH 05/18] feat(serialize-references): support open generic type selection - Offer open generic definitions (e.g. Modifier) as candidates: infer arguments from a closed-generic field, or resolve them in a new recursive, constraint-aware GenericArgumentSelectorWindow, validating the closed type against the field before assignment. Works in IMGUI and UIToolkit. - Add an optional additionalTypes pass-through to TypeSelectorWindow / HierarchyBuilder / TypeInfo, and render generic names as Modifier. --- .../SerializeReferenceIMGUIPropertyDrawer.cs | 20 +- .../Extensions/SerializeReferenceHelpers.cs | 163 +++++++++++++ .../SerializeReferences/Selectors.meta | 8 + .../GenericArgumentSelectorWindow.cs | 228 ++++++++++++++++++ .../GenericArgumentSelectorWindow.cs.meta | 11 + .../VisualElements/SerializeReferenceField.cs | 23 +- .../Types/Extensions/TypeSelectorHelpers.cs | 22 +- .../Types/Selectors/HierarchyBuilder.cs | 8 +- .../Scripts/Types/Selectors/TypeInfo.cs | 40 ++- .../Types/Selectors/TypeSelectorWindow.cs | 12 +- 10 files changed, 517 insertions(+), 18 deletions(-) create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs index bb25276c..68a92f6d 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs @@ -113,6 +113,7 @@ private static float GetChildrenHeight(SerializedProperty property, float spacin private static void ShowSelector(SerializedProperty property, Type[] types, Type currentType, Rect dropdownRect) { var persistent = property.Persistent(); + var fieldType = types.Length > 0 ? types[0] : typeof(object); var screenPosition = GUIUtility.GUIToScreenPoint(new Vector2(dropdownRect.x, dropdownRect.y)); var screenRect = new Rect(screenPosition.x, screenPosition.y, dropdownRect.width, dropdownRect.height); @@ -127,10 +128,23 @@ private static void ShowSelector(SerializedProperty property, Type[] types, Type ? null : Type.GetType(assemblyQualifiedName, throwOnError: false); - persistent.SetManagedReferenceAndApply(SerializeReferenceHelpers.CreateInstance(selectedType)); - persistent.isExpanded = selectedType is not null; + // An open generic definition needs its arguments resolved (inferred from the field or + // picked by the user) before it can be instantiated. + if (selectedType is { IsGenericTypeDefinition: true }) + SerializeReferenceHelpers.ResolveGenericType(selectedType, fieldType, screenRect, Apply); + else + Apply(selectedType); }, - filter: SerializeReferenceHelpers.IsAssignableManagedReference); + filter: SerializeReferenceHelpers.IsAssignableManagedReference, + additionalTypes: SerializeReferenceHelpers.GetAssignableGenericDefinitions(fieldType)); + + return; + + void Apply(Type type) + { + persistent.SetManagedReferenceAndApply(SerializeReferenceHelpers.CreateInstance(type)); + persistent.isExpanded = type is not null; + } } private static string GetCaption(SerializedProperty property, Type currentType) diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs index 8b7b1f5b..fb82babd 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs @@ -1,5 +1,9 @@ using System; +using System.Linq; using UnityEditor; +using UnityEngine; +using System.Reflection; +using System.Collections.Generic; using System.Runtime.Serialization; using Object = UnityEngine.Object; @@ -80,5 +84,164 @@ public static Type GetTypeFromTypename(string typename) var fullName = typename[(separator + 1)..]; return Type.GetType($"{fullName}, {assembly}", throwOnError: false); } + + #region Generics + + /// + /// Predicate identifying open generic type definitions that can be offered for a + /// [SerializeReference] field once closed over concrete arguments: non-abstract generic + /// classes that are neither nor delegates. Mirrors + /// but for the open-generic case. + /// + public static bool IsAssignableGenericDefinition(Type type) => + type is { IsClass: true, IsAbstract: false, IsGenericTypeDefinition: true } && + !typeof(Object).IsAssignableFrom(type) && + !typeof(Delegate).IsAssignableFrom(type); + + /// + /// Predicate identifying types usable as a generic argument of a serialized managed reference: + /// concrete, non-generic types Unity can serialize as a field value (primitives, , + /// enums, -derived references, or [Serializable] structs/classes). + /// + public static bool IsValidGenericArgument(Type type) + { + if (type is null) return false; + if (type.IsAbstract || type.IsInterface || type.ContainsGenericParameters) return false; + if (typeof(Delegate).IsAssignableFrom(type)) return false; + + return type.IsPrimitive || + type.IsEnum || + type == typeof(string) || + typeof(Object).IsAssignableFrom(type) || + (type.IsValueType && type.IsSerializable) || + (type.IsClass && type.IsSerializable); + } + + /// + /// Enumerates the open generic type definitions whose closed form could be assigned to + /// — i.e. generic classes that implement/inherit the field's type + /// (matched by generic definition for a generic field, or directly for a non-generic field). + /// These are offered alongside the concrete candidates; selecting one resolves its type arguments + /// via . + /// + public static IEnumerable GetAssignableGenericDefinitions(Type fieldType) + { + if (fieldType is null) yield break; + + foreach (var type in EnumerateDomainTypes()) + { + if (!IsAssignableGenericDefinition(type)) continue; + if (CanCloseToFieldType(type, fieldType)) yield return type; + } + } + + /// + /// Resolves a closed generic type from the selected open : when the + /// arguments can be inferred from a closed-generic the closed type is + /// produced directly; otherwise a is opened at + /// so the user picks each argument. The resulting closed type is passed to + /// (never invoked if the user cancels). + /// + public static void ResolveGenericType(Type openDefinition, Type fieldType, Rect anchor, Action onResolved) + { + if (openDefinition is null) return; + + if (TryMakeConcreteFromField(fieldType, openDefinition, out var closed)) + onResolved?.Invoke(closed); + else + GenericArgumentSelectorWindow.Show(anchor, openDefinition, fieldType, onResolved); + } + + /// + /// Attempts to close using the type arguments of a closed-generic + /// (e.g. a Modifier<float> field directly determines the + /// argument of a Modifier<> candidate). Returns when the field is + /// not a closed generic or the inferred type would not be assignable. + /// + public static bool TryMakeConcreteFromField(Type fieldType, Type openDefinition, out Type closed) + { + closed = null; + + if (fieldType is null || !fieldType.IsGenericType || fieldType.ContainsGenericParameters) return false; + + var fieldArguments = fieldType.GetGenericArguments(); + if (fieldArguments.Length != openDefinition.GetGenericArguments().Length) return false; + + var fieldDefinition = fieldType.GetGenericTypeDefinition(); + var matchesDefinition = GenericBaseDefinitions(openDefinition) + .Any(definition => definition == fieldDefinition); + + if (!matchesDefinition) return false; + + try + { + closed = openDefinition.MakeGenericType(fieldArguments); + } + catch (Exception) + { + closed = null; + return false; + } + + return fieldType.IsAssignableFrom(closed); + } + + private static bool CanCloseToFieldType(Type openDefinition, Type fieldType) + { + if (fieldType.IsGenericType) + { + var fieldDefinition = fieldType.GetGenericTypeDefinition(); + return GenericBaseDefinitions(openDefinition).Any(definition => definition == fieldDefinition); + } + + if (fieldType.IsAssignableFrom(openDefinition)) return true; + if (openDefinition.GetInterfaces().Contains(fieldType)) return true; + + for (var current = openDefinition.BaseType; current is not null; current = current.BaseType) + if (current == fieldType) return true; + + return false; + } + + /// + /// Enumerates the generic-type-definition view of itself, its base + /// class chain, and its interfaces (only the generic ones, reduced to their definitions). Used to + /// match a candidate generic against a generic field's definition. + /// + private static IEnumerable GenericBaseDefinitions(Type openDefinition) + { + if (openDefinition.IsGenericType) + yield return openDefinition.GetGenericTypeDefinition(); + + for (var current = openDefinition.BaseType; current is not null; current = current.BaseType) + if (current.IsGenericType) + yield return current.GetGenericTypeDefinition(); + + foreach (var contract in openDefinition.GetInterfaces()) + if (contract.IsGenericType) + yield return contract.GetGenericTypeDefinition(); + } + + private static IEnumerable EnumerateDomainTypes() + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + Type[] types; + + try + { + types = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + types = ex.Types.Where(type => type is not null).ToArray(); + } + + foreach (var type in types) + yield return type; + } + } + + #endregion } } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors.meta new file mode 100644 index 00000000..798d7e4a --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 371ce60b013cb47a58532e91e2f63e4f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs new file mode 100644 index 00000000..ca8e8289 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs @@ -0,0 +1,228 @@ +using System; +using System.Linq; +using UnityEditor; +using UnityEngine; +using System.Reflection; +using UnityEngine.UIElements; +using Aspid.FastTools.Types; +using Aspid.FastTools.Types.Editors; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.SerializeReferences.Editors +{ + /// + /// Floating editor window that resolves the type arguments of an open generic definition selected in a + /// [SerializeReferenceSelector] dropdown. It renders one row per generic parameter; each row reuses + /// the existing (hierarchy + search) to pick a concrete argument, honouring + /// the parameter's base-type and special (struct/class/new()) constraints. Pressing + /// Create closes the definition via and invokes the callback. + /// + /// + /// Shown via (not as a dropdown) so it survives the focus changes + /// caused by opening the per-parameter . + /// + internal sealed class GenericArgumentSelectorWindow : EditorWindow + { + private const string SelectArgumentText = "Select Type"; + + private Type _openDefinition; + private Type _fieldType; + private Type[] _parameters; + private Type[] _selected; + private Button[] _argumentButtons; + private Button _createButton; + private HelpBox _errorBox; + private Action _onResolved; + + /// + /// Opens the window anchored near to resolve the arguments of + /// . The constructed closed type is validated against + /// before is invoked; it is never invoked + /// if the window is closed first or the closed type is not assignable to the field. + /// + public static void Show(Rect anchor, Type openDefinition, Type fieldType, Action onResolved) + { + var window = CreateInstance(); + window.titleContent = new GUIContent("Select Generic Arguments"); + + window._openDefinition = openDefinition; + window._fieldType = fieldType; + window._parameters = openDefinition.GetGenericArguments(); + window._selected = new Type[window._parameters.Length]; + window._onResolved = onResolved; + + window.BuildUI(); + window.ShowUtility(); + + var height = 28f + window._parameters.Length * 24f + 30f; + window.position = new Rect(anchor.x, anchor.yMax, Mathf.Max(320f, anchor.width), height); + } + + private void BuildUI() + { + _argumentButtons = new Button[_parameters.Length]; + + var root = rootVisualElement; + root.Clear(); + root.style.paddingLeft = 6; + root.style.paddingRight = 6; + root.style.paddingTop = 6; + + root.Add(new Label($"Arguments for {FormatDefinitionName(_openDefinition)}") + { + style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } + }); + + for (var i = 0; i < _parameters.Length; i++) + { + var index = i; + var parameter = _parameters[i]; + + var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; + + row.Add(new Label($"[{i}] {parameter.Name}") + { + style = { minWidth = 70, marginRight = 4 } + }); + + var button = new Button { text = SelectArgumentText, style = { flexGrow = 1 } }; + button.clicked += () => ShowArgumentPicker(index, button); + + _argumentButtons[i] = button; + row.Add(button); + root.Add(row); + } + + _errorBox = new HelpBox(string.Empty, HelpBoxMessageType.Error) + { + style = { display = DisplayStyle.None, marginTop = 4 } + }; + root.Add(_errorBox); + + _createButton = new Button(CreateGenericType) { text = "Create", style = { marginTop = 6 } }; + root.Add(_createButton); + + RefreshCreateButton(); + } + + private void ShowArgumentPicker(int parameterIndex, Button button) + { + var parameter = _parameters[parameterIndex]; + var baseTypes = GetConstraintBaseTypes(parameter); + var constraintType = baseTypes.Length == 1 ? baseTypes[0] : typeof(object); + var filter = BuildArgumentFilter(parameter); + + // Offer open generic definitions as arguments too, so the user can nest generics (e.g. choose + // Modifier for T) — picking one resolves its own arguments recursively before it is used here. + var genericDefinitions = SerializeReferenceHelpers.GetAssignableGenericDefinitions(constraintType); + + var screenRect = new Rect( + position.x + button.worldBound.xMin, + position.y + button.worldBound.yMin, + button.worldBound.width, + button.worldBound.height); + + TypeSelectorWindow.Show( + screenRect: screenRect, + types: baseTypes, + currentAqn: _selected[parameterIndex]?.AssemblyQualifiedName ?? string.Empty, + allow: TypeAllow.None, + onSelected: assemblyQualifiedName => + { + var selectedType = string.IsNullOrEmpty(assemblyQualifiedName) + ? null + : Type.GetType(assemblyQualifiedName, throwOnError: false); + + if (selectedType is { IsGenericTypeDefinition: true }) + SerializeReferenceHelpers.ResolveGenericType( + selectedType, constraintType, screenRect, resolved => SetArgument(parameterIndex, resolved)); + else + SetArgument(parameterIndex, selectedType); + }, + filter: filter, + additionalTypes: genericDefinitions); + } + + private void SetArgument(int parameterIndex, Type type) + { + _selected[parameterIndex] = type; + _argumentButtons[parameterIndex].text = + type is null ? SelectArgumentText : TypeSelectorHelpers.GetTypeSelectorTitle(type); + RefreshCreateButton(); + } + + private void CreateGenericType() + { + if (_selected.Any(type => type is null)) return; + + Type closed; + try + { + closed = _openDefinition.MakeGenericType(_selected); + } + catch (Exception exception) + { + ShowError($"Cannot construct {FormatDefinitionName(_openDefinition)}: {exception.Message}"); + return; + } + + // The chosen arguments may satisfy the type parameter's own constraints yet still produce a type + // that is not assignable to the managed-reference field — guard against a value Unity would drop. + if (_fieldType is not null && !_fieldType.IsAssignableFrom(closed)) + { + ShowError($"{closed.Name} is not assignable to {_fieldType.Name}."); + return; + } + + _onResolved?.Invoke(closed); + Close(); + } + + private void ShowError(string message) + { + _errorBox.text = message; + _errorBox.style.display = DisplayStyle.Flex; + } + + private void RefreshCreateButton() + { + _createButton.SetEnabled(_selected.All(type => type is not null)); + _errorBox.style.display = DisplayStyle.None; + } + + private static Type[] GetConstraintBaseTypes(Type parameter) + { + var constraints = parameter.GetGenericParameterConstraints() + .Where(constraint => !constraint.IsGenericParameter && !constraint.ContainsGenericParameters) + .ToArray(); + + return constraints.Length > 0 ? constraints : new[] { typeof(object) }; + } + + private static Func BuildArgumentFilter(Type parameter) + { + var special = parameter.GenericParameterAttributes & GenericParameterAttributes.SpecialConstraintMask; + var requireValueType = (special & GenericParameterAttributes.NotNullableValueTypeConstraint) != 0; + var requireReferenceType = (special & GenericParameterAttributes.ReferenceTypeConstraint) != 0; + var requireDefaultCtor = (special & GenericParameterAttributes.DefaultConstructorConstraint) != 0; + + return type => + { + if (!SerializeReferenceHelpers.IsValidGenericArgument(type)) return false; + if (requireValueType && !type.IsValueType) return false; + if (requireReferenceType && type.IsValueType) return false; + + return !requireDefaultCtor || type.IsValueType || type.GetConstructor(Type.EmptyTypes) is not null; + }; + } + + private static string FormatDefinitionName(Type definition) + { + var name = definition.Name; + var tick = name.IndexOf('`'); + var baseName = tick >= 0 ? name[..tick] : name; + var arguments = string.Join(", ", definition.GetGenericArguments().Select(argument => argument.Name)); + return $"{baseName}<{arguments}>"; + } + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs.meta new file mode 100644 index 00000000..4aafb67c --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b201e7ce74c914927b40f42ff5e7f1cb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs index 68104a40..3f11ef08 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs @@ -160,9 +160,11 @@ private void OnDropdownClicked(PointerDownEvent evt) if (!window) return; var currentType = SerializeReferenceHelpers.GetCurrentType(_property); + var fieldType = _types.Length > 0 ? _types[0] : typeof(object); + var screenRect = GetScreenRect(); TypeSelectorWindow.Show( - screenRect: GetScreenRect(), + screenRect: screenRect, types: _types, currentAqn: currentType?.AssemblyQualifiedName ?? string.Empty, allow: TypeAllow.None, @@ -172,15 +174,26 @@ private void OnDropdownClicked(PointerDownEvent evt) ? null : Type.GetType(assemblyQualifiedName, throwOnError: false); - _property.SetManagedReferenceAndApply(SerializeReferenceHelpers.CreateInstance(selectedType)); - _property.isExpanded = selectedType is not null; - Refresh(forceRebuild: true); + // An open generic definition needs its arguments resolved (inferred from the field or + // picked by the user) before it can be instantiated. + if (selectedType is { IsGenericTypeDefinition: true }) + SerializeReferenceHelpers.ResolveGenericType(selectedType, fieldType, screenRect, Apply); + else + Apply(selectedType); }, - filter: SerializeReferenceHelpers.IsAssignableManagedReference); + filter: SerializeReferenceHelpers.IsAssignableManagedReference, + additionalTypes: SerializeReferenceHelpers.GetAssignableGenericDefinitions(fieldType)); evt.StopPropagation(); return; + void Apply(Type type) + { + _property.SetManagedReferenceAndApply(SerializeReferenceHelpers.CreateInstance(type)); + _property.isExpanded = type is not null; + Refresh(forceRebuild: true); + } + Rect GetScreenRect() => new( window.position.x + _dropdown.worldBound.xMin, window.position.y + _dropdown.worldBound.yMin, diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeSelectorHelpers.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeSelectorHelpers.cs index af0efccc..1d96bc1b 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeSelectorHelpers.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeSelectorHelpers.cs @@ -33,11 +33,31 @@ internal static class TypeSelectorHelpers /// public static string GetTypeSelectorTitle(Type value, string assemblyQualifiedName = null) { - if (value is not null) return value.Name; + if (value is not null) return FormatName(value); return string.IsNullOrWhiteSpace(assemblyQualifiedName) ? NoneOption : $""; } + + /// + /// Short type name with angle-bracket generic arguments (Modifier<Single>) instead of + /// the raw arity form (Modifier`1). Non-generic types are returned unchanged. + /// + private static string FormatName(Type value) + { + if (!value.IsGenericType) return value.Name; + + var name = value.Name; + var tick = name.IndexOf('`'); + var baseName = tick >= 0 ? name[..tick] : name; + + var arguments = value.GetGenericArguments(); + var argumentNames = new string[arguments.Length]; + for (var i = 0; i < arguments.Length; i++) + argumentNames[i] = arguments[i].Name; + + return $"{baseName}<{string.Join(", ", argumentNames)}>"; + } } } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs index ac3de995..fe3a6df8 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs @@ -7,9 +7,13 @@ namespace Aspid.FastTools.Types.Editors { internal static class HierarchyBuilder { - public static TreeNode Build(Type[] types, TypeAllow allow, Func filter = null) + public static TreeNode Build( + Type[] types, + TypeAllow allow, + Func filter = null, + IEnumerable additionalTypes = null) { - var allTypes = TypeInfo.GetAllTypeInfos(types, allow, filter); + var allTypes = TypeInfo.GetAllTypeInfos(types, allow, filter, additionalTypes); var root = new TreeNode("/"); root.Children.Add(new TreeNode(TypeSelectorHelpers.NoneOption, null, TypeSelectorHelpers.NoneOption)); diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs index b77232b8..f0442916 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs @@ -18,14 +18,24 @@ internal sealed class TypeInfo public TypeInfo(Type type) { - Name = type.Name; + Name = FormatName(type); FullName = type.FullName; Assembly = type.Assembly.GetName().Name; AssemblyQualifiedName = type.AssemblyQualifiedName; Namespace = string.IsNullOrEmpty(type.Namespace) ? TypeSelectorHelpers.GlobalNamespace : type.Namespace; } - - public static List GetAllTypeInfos(Type[] baseTypes, TypeAllow allow, Func filter = null) + + /// + /// Collects the type infos shown in the selector. are appended + /// verbatim (bypassing the base-type, name and checks) so callers can inject + /// entries — such as open generic definitions — that the standard + /// scan cannot match. + /// + public static List GetAllTypeInfos( + Type[] baseTypes, + TypeAllow allow, + Func filter = null, + IEnumerable additionalTypes = null) { var result = new List(); @@ -54,7 +64,31 @@ public static List GetAllTypeInfos(Type[] baseTypes, TypeAllow allow, .Select(type => new TypeInfo(type))); } + if (additionalTypes is not null) + { + var existing = new HashSet(result.Select(info => info.AssemblyQualifiedName)); + + result.AddRange(additionalTypes + .Where(type => type is not null && existing.Add(type.AssemblyQualifiedName)) + .Select(type => new TypeInfo(type))); + } + return result; } + + /// + /// Short display name for a type. Open generic definitions are rendered with angle-bracket + /// parameters (Modifier<T>) instead of Unity's raw arity form (Modifier`1). + /// + private static string FormatName(Type type) + { + if (!type.IsGenericType) return type.Name; + + var name = type.Name; + var tick = name.IndexOf('`'); + var baseName = tick >= 0 ? name[..tick] : name; + var arguments = string.Join(", ", type.GetGenericArguments().Select(argument => argument.Name)); + return $"{baseName}<{arguments}>"; + } } } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs index c10e31b2..7806f258 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs @@ -39,13 +39,15 @@ public sealed class TypeSelectorWindow : EditorWindow /// Which type kinds are included in the list. Defaults to TypeAllow.None. /// Callback invoked with the assembly-qualified name of the selected type, or null if the user chose <None>. /// Optional predicate applied to each candidate type after the base-type and checks. Return false to hide a type. Pass null to keep every matching type. + /// Optional extra types appended to the list verbatim, bypassing the base-type and checks — used to inject entries the assignability scan cannot match, such as open generic definitions. public static void Show( Rect screenRect, Type[] types = null, string currentAqn = "", TypeAllow allow = TypeAllow.None, Action onSelected = null, - Func filter = null) + Func filter = null, + IEnumerable additionalTypes = null) { types ??= new[] { typeof(object) }; @@ -56,7 +58,8 @@ public static void Show( currentAqn, allow, onSelected, - filter); + filter, + additionalTypes); } #region Initialization @@ -66,14 +69,15 @@ private void Initialize( string currentAqn, TypeAllow allow, Action onSelected, - Func filter) + Func filter, + IEnumerable additionalTypes) { _onSelected = onSelected; _currentAqn = currentAqn ?? string.Empty; BuildUI(); - var hierarchy = HierarchyBuilder.Build(types, allow, filter); + var hierarchy = HierarchyBuilder.Build(types, allow, filter, additionalTypes); InitializeNavigation(hierarchy, _currentAqn); RefreshView(); From 1237a560eebeb4b601f13d8dee9391dcc98c0a6d Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Sun, 7 Jun 2026 16:39:24 +0300 Subject: [PATCH 06/18] docs(serialize-references): add generic Modifiers example to sample - Add a non-abstract Modifier hierarchy (IModifier) with closed-generic subclasses, plus IModifier / Modifier / List fields on Loadout and IMGUILoadout to exercise open-generic selection and the T picker. - Document the generic flow in the sample README (EN/RU). --- .../Samples~/SerializeReferences/README.md | 4 ++- .../Samples~/SerializeReferences/README_RU.md | 4 ++- .../Scripts/IMGUILoadout.cs | 9 +++++++ .../SerializeReferences/Scripts/Loadout.cs | 25 ++++++++++++++++++ .../Scripts/Modifiers.meta | 8 ++++++ .../Scripts/Modifiers/AmmoModifier.cs | 14 ++++++++++ .../Scripts/Modifiers/AmmoModifier.cs.meta | 11 ++++++++ .../Scripts/Modifiers/DamageModifier.cs | 13 ++++++++++ .../Scripts/Modifiers/DamageModifier.cs.meta | 11 ++++++++ .../Scripts/Modifiers/IModifier.cs | 15 +++++++++++ .../Scripts/Modifiers/IModifier.cs.meta | 11 ++++++++ .../Scripts/Modifiers/Modifier.cs | 26 +++++++++++++++++++ .../Scripts/Modifiers/Modifier.cs.meta | 11 ++++++++ .../Scripts/Modifiers/NameModifier.cs | 13 ++++++++++ .../Scripts/Modifiers/NameModifier.cs.meta | 11 ++++++++ 15 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/AmmoModifier.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/AmmoModifier.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/DamageModifier.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/DamageModifier.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/IModifier.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/IModifier.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/Modifier.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/Modifier.cs.meta create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/NameModifier.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/NameModifier.cs.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md index 311d2165..3f310942 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README.md @@ -7,6 +7,7 @@ Look at: - `Scripts/Loadout.cs` — single (`IWeapon`), `List`, and abstract-base (`StatusEffect`) `[SerializeReference]` fields, each annotated with `[SerializeReferenceSelector]`. - `Scripts/Weapons/` — `IWeapon` interface and its implementations (`Pistol`, `Shotgun`, `Railgun`). `Railgun` nests another `[SerializeReferenceSelector]` field, showing recursive polymorphic editing. - `Scripts/Effects/` — abstract `StatusEffect` base with `BurnEffect` / `FreezeEffect`. The dropdown offers only the concrete subclasses; the abstract base is never listed. +- `Scripts/Modifiers/` — generic hierarchy: a non-abstract `Modifier` generic class (`IModifier`) with closed-generic subclasses `DamageModifier : Modifier`, `AmmoModifier : Modifier`, `NameModifier : Modifier`. An `IModifier` field offers all three subclasses **and** the open generic `Modifier` — picking `Modifier` opens a second window to choose the argument `T`. A `Modifier` field offers only the candidates assignable to it (`DamageModifier`, and `Modifier` with `T` inferred to `float`). The drawer ships both a UIToolkit and an IMGUI rendering path. The `IMGUILoadout` variant forces the IMGUI path so you can compare them or migrate IMGUI-only projects: @@ -25,7 +26,8 @@ Then experiment with the dropdowns: 2. Expand `Railgun` and change its nested `Charge Effect` to see recursive polymorphic editing. 3. Press **+** on `Sidearms` and give each element its own weapon type. 4. Open `On Hit Effect` — note only `BurnEffect` / `FreezeEffect` are offered (the abstract `StatusEffect` is hidden). -5. Right-click the component header → **Log Loadout** to print the configured loadout to the Console. +5. Open `Modifier` — the three concrete subclasses (`DamageModifier`, `AmmoModifier`, `NameModifier`) are offered alongside the open generic `Modifier`. Pick `Modifier` and a second window opens to choose the argument `T` (try `string`, then `float`) before the instance is created. Open `Float Modifier` — only candidates assignable to `Modifier` are offered (`DamageModifier`, and `Modifier` whose `T` is inferred to `float` without the extra window). +6. Right-click the component header → **Log Loadout** to print the configured loadout to the Console. Prefer building from scratch? Add an empty GameObject and attach the **Loadout** (UIToolkit) or **IMGUILoadout** (IMGUI) component. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md index 449c0751..fca63d77 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/README_RU.md @@ -7,6 +7,7 @@ - `Scripts/Loadout.cs` — одиночное поле (`IWeapon`), `List` и поле с абстрактным базовым типом (`StatusEffect`), каждое с `[SerializeReference]` и `[SerializeReferenceSelector]`. - `Scripts/Weapons/` — интерфейс `IWeapon` и его реализации (`Pistol`, `Shotgun`, `Railgun`). `Railgun` вкладывает ещё одно поле `[SerializeReferenceSelector]` — показывает рекурсивное полиморфное редактирование. - `Scripts/Effects/` — абстрактный базовый `StatusEffect` с `BurnEffect` / `FreezeEffect`. В списке предлагаются только конкретные подтипы; абстрактный базовый класс никогда не показывается. +- `Scripts/Modifiers/` — generic-иерархия: неабстрактный generic-класс `Modifier` (`IModifier`) с закрытыми подтипами `DamageModifier : Modifier`, `AmmoModifier : Modifier`, `NameModifier : Modifier`. Поле `IModifier` предлагает все три подтипа **и** сам открытый `Modifier` — при его выборе открывается второе окно для выбора аргумента `T`. Поле `Modifier` предлагает только присваиваемых кандидатов (`DamageModifier` и `Modifier` с выведенным `T = float`). Drawer поддерживает и UIToolkit, и IMGUI. Вариант `IMGUILoadout` принудительно использует IMGUI-путь — удобно для сравнения или миграции IMGUI-проектов: @@ -25,7 +26,8 @@ Drawer поддерживает и UIToolkit, и IMGUI. Вариант `IMGUILoa 2. Разверните `Railgun` и смените вложенный `Charge Effect` — увидите рекурсивное полиморфное редактирование. 3. Нажмите **+** на `Sidearms` и задайте каждому элементу свой тип оружия. 4. Откройте `On Hit Effect` — обратите внимание, что предлагаются только `BurnEffect` / `FreezeEffect` (абстрактный `StatusEffect` скрыт). -5. ПКМ по заголовку компонента → **Log Loadout**, чтобы вывести настроенное снаряжение в Console. +5. Откройте `Modifier` — рядом с тремя конкретными подтипами (`DamageModifier`, `AmmoModifier`, `NameModifier`) предлагается и открытый `Modifier`. Выберите `Modifier` — откроется второе окно для выбора аргумента `T` (попробуйте `string`, затем `float`), и только потом создастся экземпляр. Откройте `Float Modifier` — предлагаются только присваиваемые к `Modifier` кандидаты (`DamageModifier` и `Modifier` с выведенным `T = float`, без дополнительного окна). +6. ПКМ по заголовку компонента → **Log Loadout**, чтобы вывести настроенное снаряжение в Console. Хотите собрать с нуля? Добавьте пустой GameObject и прикрепите компонент **Loadout** (UIToolkit) или **IMGUILoadout** (IMGUI). diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs index ab12c6fb..c88d0f10 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/IMGUILoadout.cs @@ -21,5 +21,14 @@ [SerializeReference] [SerializeReferenceSelector] [SerializeReference] [SerializeReferenceSelector] private StatusEffect _onHitEffect; + + [SerializeReference] [SerializeReferenceSelector] + private IModifier _modifier; + + [SerializeReference] [SerializeReferenceSelector] + private Modifier _floatModifier; + + [SerializeReference] [SerializeReferenceSelector] + private List _modifiers = new(); } } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs index 728b4069..60118ddd 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Loadout.cs @@ -12,6 +12,10 @@ namespace Aspid.FastTools.Samples.SerializeReferences // - single field → pick one IWeapon implementation // - List / array → each element is its own polymorphic picker // - abstract base type → only concrete subclasses are offered + // - generic hierarchy → concrete closed-generic subclasses are offered; the open generic + // Modifier is also offered and opens a second window to pick T; + // a closed-generic field type (Modifier) constrains candidates + // by assignability and infers T directly // // Picking a type instantiates it, clears the reference, and the assigned instance's // serialized fields appear inline under the foldout. Nested [SerializeReference] fields @@ -30,6 +34,22 @@ [SerializeReference] [SerializeReferenceSelector] [SerializeReference] [SerializeReferenceSelector] private StatusEffect _onHitEffect; + // Generic hierarchy. Non-generic IModifier field: the picker offers the concrete subclasses + // (DamageModifier, AmmoModifier, NameModifier) AND the open generic Modifier — choosing the + // latter opens a second window to pick T (e.g. string vs float). + [SerializeReference] [SerializeReferenceSelector] + private IModifier _modifier; + + // Closed-generic field type: only types assignable to Modifier are offered — + // DamageModifier (Modifier) and Modifier (its T is inferred to float, no extra window). + // AmmoModifier (int) and NameModifier (string) are excluded. + [SerializeReference] [SerializeReferenceSelector] + private Modifier _floatModifier; + + // Polymorphic list mixing different closed-generic subclasses. + [SerializeReference] [SerializeReferenceSelector] + private List _modifiers = new(); + [ContextMenu("Log Loadout")] private void LogLoadout() { @@ -39,6 +59,11 @@ private void LogLoadout() Debug.Log($"Sidearm {i}: {_sidearms[i]?.Describe() ?? "none"}"); Debug.Log($"On-hit effect: {_onHitEffect?.Describe() ?? "none"}"); + Debug.Log($"Modifier: {_modifier?.Describe() ?? "none"}"); + Debug.Log($"Float modifier: {_floatModifier?.Describe() ?? "none"}"); + + for (var i = 0; i < _modifiers.Count; i++) + Debug.Log($"Modifier {i}: {_modifiers[i]?.Describe() ?? "none"}"); } } } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers.meta new file mode 100644 index 00000000..18cfcbda --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9100055084444cdf8e2f37ff3b613c02 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/AmmoModifier.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/AmmoModifier.cs new file mode 100644 index 00000000..1f33dc51 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/AmmoModifier.cs @@ -0,0 +1,14 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + // Concrete subclass closing Modifier over int. + // Offered for an IModifier field, but NOT for a Modifier field — + // it is Modifier, which is not assignable to Modifier. + [Serializable] + public sealed class AmmoModifier : Modifier + { + public override string Describe() => $"+{Value} ammo"; + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/AmmoModifier.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/AmmoModifier.cs.meta new file mode 100644 index 00000000..63c3ed5e --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/AmmoModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 350225ab4907402aa855efd9c953246f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/DamageModifier.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/DamageModifier.cs new file mode 100644 index 00000000..9d31af86 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/DamageModifier.cs @@ -0,0 +1,13 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + // Concrete subclass closing Modifier over float. + // Offered wherever the field type is IModifier or Modifier. + [Serializable] + public sealed class DamageModifier : Modifier + { + public override string Describe() => $"Damage ×{Value}"; + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/DamageModifier.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/DamageModifier.cs.meta new file mode 100644 index 00000000..68f58708 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/DamageModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b5d6a98f98bf40aa8f7e296f383e265b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/IModifier.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/IModifier.cs new file mode 100644 index 00000000..b30c90e3 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/IModifier.cs @@ -0,0 +1,15 @@ +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + // Non-generic entry point for the generic [SerializeReference] sample. + // + // A field typed as IModifier lets [SerializeReferenceSelector] offer both: + // - every concrete subclass that closes Modifier over a real type argument + // (DamageModifier : Modifier, AmmoModifier : Modifier, NameModifier : Modifier), and + // - the open generic Modifier itself — picking it opens a second window to choose the argument T, + // then instantiates Modifier / Modifier / etc. + public interface IModifier + { + string Describe(); + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/IModifier.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/IModifier.cs.meta new file mode 100644 index 00000000..04f0fbe3 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/IModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3864ea1917414755add52a58178a20e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/Modifier.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/Modifier.cs new file mode 100644 index 00000000..1a34f946 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/Modifier.cs @@ -0,0 +1,26 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + // Non-abstract generic base for the [SerializeReferenceSelector] generic test. + // + // Because it is a concrete open generic, [SerializeReferenceSelector] lists it as "Modifier". + // - On a non-generic IModifier field, picking it opens a second window to choose the argument T + // (e.g. string in one case, float in another), then instantiates Modifier / Modifier. + // - On a closed-generic field such as Modifier, the argument is inferred from the field, so it + // is created directly as Modifier without the extra window. + // + // The typed _value field verifies that Unity's generic serialization handles a bare type-parameter + // field (for float/int/string) and renders it inline under the dropdown. + [Serializable] + public class Modifier : IModifier + { + [SerializeField] private T _value; + + protected T Value => _value; + + public virtual string Describe() => $"Modifier<{typeof(T).Name}> = {_value}"; + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/Modifier.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/Modifier.cs.meta new file mode 100644 index 00000000..d8e7bd3c --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/Modifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dcde013178d3400892d1f76d1b8a1cb2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/NameModifier.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/NameModifier.cs new file mode 100644 index 00000000..2c618c88 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/NameModifier.cs @@ -0,0 +1,13 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Samples.SerializeReferences +{ + // Concrete subclass closing Modifier over string. + // Offered for an IModifier field; excluded from a Modifier field. + [Serializable] + public sealed class NameModifier : Modifier + { + public override string Describe() => $"Renamed to \"{Value}\""; + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/NameModifier.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/NameModifier.cs.meta new file mode 100644 index 00000000..32be9d14 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/SerializeReferences/Scripts/Modifiers/NameModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1557d1630f2245b289641510ed2e6021 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 2d380e9402a9f4c5b8eb637f9f40c400, type: 3} + userData: + assetBundleName: + assetBundleVariant: From 83baf4e63c2e8edd0caf40a4b6773f3676659f17 Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Sun, 7 Jun 2026 16:39:32 +0300 Subject: [PATCH 07/18] docs(serialize-references): document open generic type selection - Note open-generic support and the new additionalTypes parameter in the CHANGELOG, and add a generic bullet to the feature section of all four READMEs. --- .../Packages/tech.aspid.fasttools/Documentation/EN/README.md | 1 + .../Packages/tech.aspid.fasttools/Documentation/RU/README.md | 1 + CHANGELOG.md | 3 ++- README.md | 1 + README_RU.md | 1 + 5 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/EN/README.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/EN/README.md index 9e045b50..8d2ba311 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/EN/README.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/EN/README.md @@ -336,6 +336,7 @@ A drop-in dropdown for `[SerializeReference]` fields. Add `[SerializeReferenceSe - Picking a type instantiates it; `` clears the reference. - The assigned instance's serialized fields are drawn inline under a foldout. - A stored type that no longer resolves (renamed or deleted) is surfaced as a missing-type warning instead of silently clearing. +- Open generic implementations (e.g. `Modifier`) are offered too: arguments are inferred from a closed-generic field, or picked in a follow-up window (validated against the field type) before instantiation. - Works on single fields, arrays, and `List`, in both IMGUI and UIToolkit inspectors. ```csharp diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/RU/README.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/RU/README.md index 52108e98..25dbd575 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/RU/README.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/RU/README.md @@ -336,6 +336,7 @@ public sealed class TankEnemy : EnemyBase - Выбор типа создаёт его экземпляр; `` очищает ссылку. - Сериализуемые поля назначенного экземпляра рисуются вложенно под foldout. - Сохранённый тип, который больше не разрешается (переименован или удалён), показывается как предупреждение о потерянном типе, а не очищается молча. +- Открытые generic-реализации (например, `Modifier`) тоже предлагаются: аргументы выводятся из закрытого generic-поля либо выбираются в дополнительном окне (с проверкой на присваиваемость полю) перед созданием экземпляра. - Работает с одиночными полями, массивами и `List`, в инспекторах IMGUI и UIToolkit. ```csharp diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b1c3658..fdd1af89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `[SerializeReferenceSelector]` attribute and property drawer — a hierarchical type-selector dropdown for `[SerializeReference]` fields (and arrays / `List` of them). Picking a concrete implementation instantiates it, `` clears the reference, the assigned instance's nested properties are drawn inline under a foldout, and a stored type that no longer resolves is surfaced as a missing-type warning. Works in both IMGUI and UIToolkit inspectors and reuses the existing `TypeSelectorWindow`. -- `TypeSelectorWindow.Show` gained an optional `filter` predicate that further narrows the candidate list after the base-type and `TypeAllow` checks (used by the SerializeReference drawer to exclude `UnityEngine.Object`, open generics, strings and delegates). +- `[SerializeReferenceSelector]` now also offers open generic implementations (e.g. `Modifier`). When the type arguments can be inferred from a closed-generic field (`Modifier`) the closed type is created directly; otherwise a follow-up window lets you pick each argument (honouring the parameter's constraints) and validates the result against the field type before instantiating. Works in both IMGUI and UIToolkit paths. +- `TypeSelectorWindow.Show` gained an optional `filter` predicate that further narrows the candidate list after the base-type and `TypeAllow` checks (used by the SerializeReference drawer to exclude `UnityEngine.Object`, strings and delegates), plus an optional `additionalTypes` parameter for injecting entries the assignability scan cannot match (such as open generic definitions). ## [1.0.0-rc.5] — 2026-06-06 diff --git a/README.md b/README.md index 45d57f32..89627d25 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,7 @@ A drop-in dropdown for `[SerializeReference]` fields. Add `[SerializeReferenceSe - Picking a type instantiates it; `` clears the reference. - The assigned instance's serialized fields are drawn inline under a foldout. - A stored type that no longer resolves (renamed or deleted) is surfaced as a missing-type warning instead of silently clearing. +- Open generic implementations (e.g. `Modifier`) are offered too: arguments are inferred from a closed-generic field, or picked in a follow-up window (validated against the field type) before instantiation. - Works on single fields, arrays, and `List`, in both IMGUI and UIToolkit inspectors. ```csharp diff --git a/README_RU.md b/README_RU.md index aee6cc9f..93b485f0 100644 --- a/README_RU.md +++ b/README_RU.md @@ -339,6 +339,7 @@ public sealed class TankEnemy : EnemyBase - Выбор типа создаёт его экземпляр; `` очищает ссылку. - Сериализуемые поля назначенного экземпляра рисуются вложенно под foldout. - Сохранённый тип, который больше не разрешается (переименован или удалён), показывается как предупреждение о потерянном типе, а не очищается молча. +- Открытые generic-реализации (например, `Modifier`) тоже предлагаются: аргументы выводятся из закрытого generic-поля либо выбираются в дополнительном окне (с проверкой на присваиваемость полю) перед созданием экземпляра. - Работает с одиночными полями, массивами и `List`, в инспекторах IMGUI и UIToolkit. ```csharp From 6653fc86ca5c1680c6e7101f5d4a5588eb640c01 Mon Sep 17 00:00:00 2001 From: Vladislav Panin Date: Sun, 7 Jun 2026 20:44:36 +0300 Subject: [PATCH 08/18] refactor(serialize-references): resolve generic arguments inline in the dropdown - Resolve open generics inside TypeSelectorWindow as in-window argument pages (hierarchy, search, breadcrumb, live preview), removing the separate GenericArgumentSelectorWindow and its focus issues - Extract generic resolution into GenericTypeResolver and add an argumentFilter parameter to TypeSelectorWindow.Show; the flow stays dormant unless open generics are present - Format generic type names recursively so nested closed generics render fully (Modifier>) - Resolve the generic type definition when locating a script so Open Script works for closed generics --- .../SerializeReferenceIMGUIPropertyDrawer.cs | 19 +- .../Extensions/SerializeReferenceHelpers.cs | 152 +------- .../SerializeReferences/Selectors.meta | 8 - .../GenericArgumentSelectorWindow.cs | 228 ------------ .../VisualElements/SerializeReferenceField.cs | 19 +- .../Types/Extensions/TypeExtensions.cs | 22 +- .../Types/Extensions/TypeSelectorHelpers.cs | 6 +- .../Types/Selectors/GenericTypeResolver.cs | 218 ++++++++++++ .../Selectors/GenericTypeResolver.cs.meta} | 2 +- .../Types/Selectors/HierarchyBuilder.cs | 8 +- .../Scripts/Types/Selectors/TypeInfo.cs | 5 +- .../Types/Selectors/TypeSelectorWindow.cs | 324 ++++++++++++++++-- 12 files changed, 551 insertions(+), 460 deletions(-) delete mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors.meta delete mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs create mode 100644 Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/GenericTypeResolver.cs rename Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/{SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs.meta => Types/Selectors/GenericTypeResolver.cs.meta} (86%) diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs index 68a92f6d..9566e2fa 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Drawers/SerializeReferenceIMGUIPropertyDrawer.cs @@ -122,21 +122,12 @@ private static void ShowSelector(SerializedProperty property, Type[] types, Type types: types, currentAqn: currentType?.AssemblyQualifiedName ?? string.Empty, allow: TypeAllow.None, - onSelected: assemblyQualifiedName => - { - var selectedType = string.IsNullOrEmpty(assemblyQualifiedName) - ? null - : Type.GetType(assemblyQualifiedName, throwOnError: false); - - // An open generic definition needs its arguments resolved (inferred from the field or - // picked by the user) before it can be instantiated. - if (selectedType is { IsGenericTypeDefinition: true }) - SerializeReferenceHelpers.ResolveGenericType(selectedType, fieldType, screenRect, Apply); - else - Apply(selectedType); - }, + onSelected: assemblyQualifiedName => Apply(string.IsNullOrEmpty(assemblyQualifiedName) + ? null + : Type.GetType(assemblyQualifiedName, throwOnError: false)), filter: SerializeReferenceHelpers.IsAssignableManagedReference, - additionalTypes: SerializeReferenceHelpers.GetAssignableGenericDefinitions(fieldType)); + additionalTypes: GenericTypeResolver.GetAssignableGenericDefinitions(fieldType), + argumentFilter: SerializeReferenceHelpers.IsValidGenericArgument); return; diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs index fb82babd..3826ded9 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs @@ -1,9 +1,5 @@ using System; -using System.Linq; using UnityEditor; -using UnityEngine; -using System.Reflection; -using System.Collections.Generic; using System.Runtime.Serialization; using Object = UnityEngine.Object; @@ -13,7 +9,10 @@ namespace Aspid.FastTools.SerializeReferences.Editors /// /// Shared helpers for the [SerializeReferenceSelector] drawers: resolving the declared /// managed-reference field type, filtering candidate types, instantiating the selected type, and - /// parsing Unity's managed-reference type-name format. + /// parsing Unity's managed-reference type-name format. The open-generic argument flow itself lives in + /// the shared / + /// ; + /// is supplied to the selector as its argument filter. /// internal static class SerializeReferenceHelpers { @@ -85,23 +84,11 @@ public static Type GetTypeFromTypename(string typename) return Type.GetType($"{fullName}, {assembly}", throwOnError: false); } - #region Generics - - /// - /// Predicate identifying open generic type definitions that can be offered for a - /// [SerializeReference] field once closed over concrete arguments: non-abstract generic - /// classes that are neither nor delegates. Mirrors - /// but for the open-generic case. - /// - public static bool IsAssignableGenericDefinition(Type type) => - type is { IsClass: true, IsAbstract: false, IsGenericTypeDefinition: true } && - !typeof(Object).IsAssignableFrom(type) && - !typeof(Delegate).IsAssignableFrom(type); - /// /// Predicate identifying types usable as a generic argument of a serialized managed reference: /// concrete, non-generic types Unity can serialize as a field value (primitives, , - /// enums, -derived references, or [Serializable] structs/classes). + /// enums, -derived references, or [Serializable] structs/classes). Passed to + /// as the argument filter. /// public static bool IsValidGenericArgument(Type type) { @@ -116,132 +103,5 @@ public static bool IsValidGenericArgument(Type type) (type.IsValueType && type.IsSerializable) || (type.IsClass && type.IsSerializable); } - - /// - /// Enumerates the open generic type definitions whose closed form could be assigned to - /// — i.e. generic classes that implement/inherit the field's type - /// (matched by generic definition for a generic field, or directly for a non-generic field). - /// These are offered alongside the concrete candidates; selecting one resolves its type arguments - /// via . - /// - public static IEnumerable GetAssignableGenericDefinitions(Type fieldType) - { - if (fieldType is null) yield break; - - foreach (var type in EnumerateDomainTypes()) - { - if (!IsAssignableGenericDefinition(type)) continue; - if (CanCloseToFieldType(type, fieldType)) yield return type; - } - } - - /// - /// Resolves a closed generic type from the selected open : when the - /// arguments can be inferred from a closed-generic the closed type is - /// produced directly; otherwise a is opened at - /// so the user picks each argument. The resulting closed type is passed to - /// (never invoked if the user cancels). - /// - public static void ResolveGenericType(Type openDefinition, Type fieldType, Rect anchor, Action onResolved) - { - if (openDefinition is null) return; - - if (TryMakeConcreteFromField(fieldType, openDefinition, out var closed)) - onResolved?.Invoke(closed); - else - GenericArgumentSelectorWindow.Show(anchor, openDefinition, fieldType, onResolved); - } - - /// - /// Attempts to close using the type arguments of a closed-generic - /// (e.g. a Modifier<float> field directly determines the - /// argument of a Modifier<> candidate). Returns when the field is - /// not a closed generic or the inferred type would not be assignable. - /// - public static bool TryMakeConcreteFromField(Type fieldType, Type openDefinition, out Type closed) - { - closed = null; - - if (fieldType is null || !fieldType.IsGenericType || fieldType.ContainsGenericParameters) return false; - - var fieldArguments = fieldType.GetGenericArguments(); - if (fieldArguments.Length != openDefinition.GetGenericArguments().Length) return false; - - var fieldDefinition = fieldType.GetGenericTypeDefinition(); - var matchesDefinition = GenericBaseDefinitions(openDefinition) - .Any(definition => definition == fieldDefinition); - - if (!matchesDefinition) return false; - - try - { - closed = openDefinition.MakeGenericType(fieldArguments); - } - catch (Exception) - { - closed = null; - return false; - } - - return fieldType.IsAssignableFrom(closed); - } - - private static bool CanCloseToFieldType(Type openDefinition, Type fieldType) - { - if (fieldType.IsGenericType) - { - var fieldDefinition = fieldType.GetGenericTypeDefinition(); - return GenericBaseDefinitions(openDefinition).Any(definition => definition == fieldDefinition); - } - - if (fieldType.IsAssignableFrom(openDefinition)) return true; - if (openDefinition.GetInterfaces().Contains(fieldType)) return true; - - for (var current = openDefinition.BaseType; current is not null; current = current.BaseType) - if (current == fieldType) return true; - - return false; - } - - /// - /// Enumerates the generic-type-definition view of itself, its base - /// class chain, and its interfaces (only the generic ones, reduced to their definitions). Used to - /// match a candidate generic against a generic field's definition. - /// - private static IEnumerable GenericBaseDefinitions(Type openDefinition) - { - if (openDefinition.IsGenericType) - yield return openDefinition.GetGenericTypeDefinition(); - - for (var current = openDefinition.BaseType; current is not null; current = current.BaseType) - if (current.IsGenericType) - yield return current.GetGenericTypeDefinition(); - - foreach (var contract in openDefinition.GetInterfaces()) - if (contract.IsGenericType) - yield return contract.GetGenericTypeDefinition(); - } - - private static IEnumerable EnumerateDomainTypes() - { - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - Type[] types; - - try - { - types = assembly.GetTypes(); - } - catch (ReflectionTypeLoadException ex) - { - types = ex.Types.Where(type => type is not null).ToArray(); - } - - foreach (var type in types) - yield return type; - } - } - - #endregion } } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors.meta deleted file mode 100644 index 798d7e4a..00000000 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 371ce60b013cb47a58532e91e2f63e4f -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs deleted file mode 100644 index ca8e8289..00000000 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Linq; -using UnityEditor; -using UnityEngine; -using System.Reflection; -using UnityEngine.UIElements; -using Aspid.FastTools.Types; -using Aspid.FastTools.Types.Editors; - -// ReSharper disable once CheckNamespace -namespace Aspid.FastTools.SerializeReferences.Editors -{ - /// - /// Floating editor window that resolves the type arguments of an open generic definition selected in a - /// [SerializeReferenceSelector] dropdown. It renders one row per generic parameter; each row reuses - /// the existing (hierarchy + search) to pick a concrete argument, honouring - /// the parameter's base-type and special (struct/class/new()) constraints. Pressing - /// Create closes the definition via and invokes the callback. - /// - /// - /// Shown via (not as a dropdown) so it survives the focus changes - /// caused by opening the per-parameter . - /// - internal sealed class GenericArgumentSelectorWindow : EditorWindow - { - private const string SelectArgumentText = "Select Type"; - - private Type _openDefinition; - private Type _fieldType; - private Type[] _parameters; - private Type[] _selected; - private Button[] _argumentButtons; - private Button _createButton; - private HelpBox _errorBox; - private Action _onResolved; - - /// - /// Opens the window anchored near to resolve the arguments of - /// . The constructed closed type is validated against - /// before is invoked; it is never invoked - /// if the window is closed first or the closed type is not assignable to the field. - /// - public static void Show(Rect anchor, Type openDefinition, Type fieldType, Action onResolved) - { - var window = CreateInstance(); - window.titleContent = new GUIContent("Select Generic Arguments"); - - window._openDefinition = openDefinition; - window._fieldType = fieldType; - window._parameters = openDefinition.GetGenericArguments(); - window._selected = new Type[window._parameters.Length]; - window._onResolved = onResolved; - - window.BuildUI(); - window.ShowUtility(); - - var height = 28f + window._parameters.Length * 24f + 30f; - window.position = new Rect(anchor.x, anchor.yMax, Mathf.Max(320f, anchor.width), height); - } - - private void BuildUI() - { - _argumentButtons = new Button[_parameters.Length]; - - var root = rootVisualElement; - root.Clear(); - root.style.paddingLeft = 6; - root.style.paddingRight = 6; - root.style.paddingTop = 6; - - root.Add(new Label($"Arguments for {FormatDefinitionName(_openDefinition)}") - { - style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } - }); - - for (var i = 0; i < _parameters.Length; i++) - { - var index = i; - var parameter = _parameters[i]; - - var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; - - row.Add(new Label($"[{i}] {parameter.Name}") - { - style = { minWidth = 70, marginRight = 4 } - }); - - var button = new Button { text = SelectArgumentText, style = { flexGrow = 1 } }; - button.clicked += () => ShowArgumentPicker(index, button); - - _argumentButtons[i] = button; - row.Add(button); - root.Add(row); - } - - _errorBox = new HelpBox(string.Empty, HelpBoxMessageType.Error) - { - style = { display = DisplayStyle.None, marginTop = 4 } - }; - root.Add(_errorBox); - - _createButton = new Button(CreateGenericType) { text = "Create", style = { marginTop = 6 } }; - root.Add(_createButton); - - RefreshCreateButton(); - } - - private void ShowArgumentPicker(int parameterIndex, Button button) - { - var parameter = _parameters[parameterIndex]; - var baseTypes = GetConstraintBaseTypes(parameter); - var constraintType = baseTypes.Length == 1 ? baseTypes[0] : typeof(object); - var filter = BuildArgumentFilter(parameter); - - // Offer open generic definitions as arguments too, so the user can nest generics (e.g. choose - // Modifier for T) — picking one resolves its own arguments recursively before it is used here. - var genericDefinitions = SerializeReferenceHelpers.GetAssignableGenericDefinitions(constraintType); - - var screenRect = new Rect( - position.x + button.worldBound.xMin, - position.y + button.worldBound.yMin, - button.worldBound.width, - button.worldBound.height); - - TypeSelectorWindow.Show( - screenRect: screenRect, - types: baseTypes, - currentAqn: _selected[parameterIndex]?.AssemblyQualifiedName ?? string.Empty, - allow: TypeAllow.None, - onSelected: assemblyQualifiedName => - { - var selectedType = string.IsNullOrEmpty(assemblyQualifiedName) - ? null - : Type.GetType(assemblyQualifiedName, throwOnError: false); - - if (selectedType is { IsGenericTypeDefinition: true }) - SerializeReferenceHelpers.ResolveGenericType( - selectedType, constraintType, screenRect, resolved => SetArgument(parameterIndex, resolved)); - else - SetArgument(parameterIndex, selectedType); - }, - filter: filter, - additionalTypes: genericDefinitions); - } - - private void SetArgument(int parameterIndex, Type type) - { - _selected[parameterIndex] = type; - _argumentButtons[parameterIndex].text = - type is null ? SelectArgumentText : TypeSelectorHelpers.GetTypeSelectorTitle(type); - RefreshCreateButton(); - } - - private void CreateGenericType() - { - if (_selected.Any(type => type is null)) return; - - Type closed; - try - { - closed = _openDefinition.MakeGenericType(_selected); - } - catch (Exception exception) - { - ShowError($"Cannot construct {FormatDefinitionName(_openDefinition)}: {exception.Message}"); - return; - } - - // The chosen arguments may satisfy the type parameter's own constraints yet still produce a type - // that is not assignable to the managed-reference field — guard against a value Unity would drop. - if (_fieldType is not null && !_fieldType.IsAssignableFrom(closed)) - { - ShowError($"{closed.Name} is not assignable to {_fieldType.Name}."); - return; - } - - _onResolved?.Invoke(closed); - Close(); - } - - private void ShowError(string message) - { - _errorBox.text = message; - _errorBox.style.display = DisplayStyle.Flex; - } - - private void RefreshCreateButton() - { - _createButton.SetEnabled(_selected.All(type => type is not null)); - _errorBox.style.display = DisplayStyle.None; - } - - private static Type[] GetConstraintBaseTypes(Type parameter) - { - var constraints = parameter.GetGenericParameterConstraints() - .Where(constraint => !constraint.IsGenericParameter && !constraint.ContainsGenericParameters) - .ToArray(); - - return constraints.Length > 0 ? constraints : new[] { typeof(object) }; - } - - private static Func BuildArgumentFilter(Type parameter) - { - var special = parameter.GenericParameterAttributes & GenericParameterAttributes.SpecialConstraintMask; - var requireValueType = (special & GenericParameterAttributes.NotNullableValueTypeConstraint) != 0; - var requireReferenceType = (special & GenericParameterAttributes.ReferenceTypeConstraint) != 0; - var requireDefaultCtor = (special & GenericParameterAttributes.DefaultConstructorConstraint) != 0; - - return type => - { - if (!SerializeReferenceHelpers.IsValidGenericArgument(type)) return false; - if (requireValueType && !type.IsValueType) return false; - if (requireReferenceType && type.IsValueType) return false; - - return !requireDefaultCtor || type.IsValueType || type.GetConstructor(Type.EmptyTypes) is not null; - }; - } - - private static string FormatDefinitionName(Type definition) - { - var name = definition.Name; - var tick = name.IndexOf('`'); - var baseName = tick >= 0 ? name[..tick] : name; - var arguments = string.Join(", ", definition.GetGenericArguments().Select(argument => argument.Name)); - return $"{baseName}<{arguments}>"; - } - } -} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs index 3f11ef08..b8a194a6 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/VisualElements/SerializeReferenceField.cs @@ -168,21 +168,12 @@ private void OnDropdownClicked(PointerDownEvent evt) types: _types, currentAqn: currentType?.AssemblyQualifiedName ?? string.Empty, allow: TypeAllow.None, - onSelected: assemblyQualifiedName => - { - var selectedType = string.IsNullOrEmpty(assemblyQualifiedName) - ? null - : Type.GetType(assemblyQualifiedName, throwOnError: false); - - // An open generic definition needs its arguments resolved (inferred from the field or - // picked by the user) before it can be instantiated. - if (selectedType is { IsGenericTypeDefinition: true }) - SerializeReferenceHelpers.ResolveGenericType(selectedType, fieldType, screenRect, Apply); - else - Apply(selectedType); - }, + onSelected: assemblyQualifiedName => Apply(string.IsNullOrEmpty(assemblyQualifiedName) + ? null + : Type.GetType(assemblyQualifiedName, throwOnError: false)), filter: SerializeReferenceHelpers.IsAssignableManagedReference, - additionalTypes: SerializeReferenceHelpers.GetAssignableGenericDefinitions(fieldType)); + additionalTypes: GenericTypeResolver.GetAssignableGenericDefinitions(fieldType), + argumentFilter: SerializeReferenceHelpers.IsValidGenericArgument); evt.StopPropagation(); return; diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeExtensions.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeExtensions.cs index ee98c977..be06018b 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeExtensions.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeExtensions.cs @@ -28,9 +28,14 @@ public static class TypeExtensions /// public static MonoScript FindMonoScript(this Type type) { - var isEnum = type.IsEnum; - var typeName = type.Name; - var typeNamespace = type.Namespace; + if (type is null) return null; + + // A closed generic (e.g. Modifier>) has no script of its own — the source file + // declares the open definition, so look that up and match by its arity-stripped name. + var lookupType = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + var isEnum = lookupType.IsEnum; + var typeName = StripArity(lookupType.Name); + var typeNamespace = lookupType.Namespace; var scripts = AssetDatabase.FindAssets(filter: $"t:MonoScript {typeName}") .Select(AssetDatabase.GUIDToAssetPath) @@ -42,7 +47,7 @@ public static MonoScript FindMonoScript(this Type type) foreach (var script in scripts) { - if (script.GetClass() != type) continue; + if (script.GetClass() != lookupType) continue; return script; } @@ -92,9 +97,16 @@ public static (MonoScript script, int line) FindMonoScriptWithLine(this Type typ var script = type.FindMonoScript(); if (script is null) return (script: null, line: 0); - var line = FindTypeLineNumber(script.text, type.Name, type.IsEnum); + var lookupType = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + var line = FindTypeLineNumber(script.text, StripArity(lookupType.Name), lookupType.IsEnum); return (script, line); } + + private static string StripArity(string name) + { + var tick = name.IndexOf('`'); + return tick >= 0 ? name[..tick] : name; + } private static int FindTypeLineNumber(string text, string typeName, bool isEnum) { diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeSelectorHelpers.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeSelectorHelpers.cs index 1d96bc1b..35511dac 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeSelectorHelpers.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Extensions/TypeSelectorHelpers.cs @@ -42,7 +42,9 @@ public static string GetTypeSelectorTitle(Type value, string assemblyQualifiedNa /// /// Short type name with angle-bracket generic arguments (Modifier<Single>) instead of - /// the raw arity form (Modifier`1). Non-generic types are returned unchanged. + /// the raw arity form (Modifier`1). Generic arguments are formatted recursively, so a nested + /// closed generic renders fully (Modifier<Modifier<Int32>>). Non-generic types + /// are returned unchanged. /// private static string FormatName(Type value) { @@ -55,7 +57,7 @@ private static string FormatName(Type value) var arguments = value.GetGenericArguments(); var argumentNames = new string[arguments.Length]; for (var i = 0; i < arguments.Length; i++) - argumentNames[i] = arguments[i].Name; + argumentNames[i] = FormatName(arguments[i]); return $"{baseName}<{string.Join(", ", argumentNames)}>"; } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/GenericTypeResolver.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/GenericTypeResolver.cs new file mode 100644 index 00000000..f8cb7715 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/GenericTypeResolver.cs @@ -0,0 +1,218 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Collections.Generic; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.Types.Editors +{ + /// + /// General-purpose helpers for resolving an open generic type definition into a concrete closed type + /// inside the argument-selection flow: enumerating the open generic + /// definitions assignable to a field, inferring arguments from a closed-generic field, computing a + /// parameter's constraint base types and special-constraint filter, and constructing/validating the + /// closed type. Carries no dependency on any particular feature (e.g. [SerializeReference]); + /// the Unity-serializability of an argument is supplied by the caller as a separate filter. + /// + internal static class GenericTypeResolver + { + /// + /// Predicate identifying open generic type definitions that can be offered for a field once closed + /// over concrete arguments: non-abstract generic classes that are neither + /// nor delegates. + /// + public static bool IsAssignableGenericDefinition(Type type) => + type is { IsClass: true, IsAbstract: false, IsGenericTypeDefinition: true } && + !typeof(UnityEngine.Object).IsAssignableFrom(type) && + !typeof(Delegate).IsAssignableFrom(type); + + /// + /// Enumerates the open generic type definitions whose closed form could be assigned to + /// — i.e. generic classes that implement/inherit the field's type + /// (matched by generic definition for a generic field, or directly for a non-generic field). + /// + public static IEnumerable GetAssignableGenericDefinitions(Type fieldType) + { + if (fieldType is null) yield break; + + foreach (var type in EnumerateDomainTypes()) + { + if (!IsAssignableGenericDefinition(type)) continue; + if (CanCloseToFieldType(type, fieldType)) yield return type; + } + } + + /// + /// Attempts to close using the type arguments of a closed-generic + /// (e.g. a Modifier<float> field directly determines the + /// argument of a Modifier<> candidate). Returns when the field is + /// not a closed generic or the inferred type would not be assignable. + /// + public static bool TryInferFromFieldType(Type fieldType, Type openDefinition, out Type closed) + { + closed = null; + + if (fieldType is null || !fieldType.IsGenericType || fieldType.ContainsGenericParameters) return false; + + var fieldArguments = fieldType.GetGenericArguments(); + if (fieldArguments.Length != openDefinition.GetGenericArguments().Length) return false; + + var fieldDefinition = fieldType.GetGenericTypeDefinition(); + var matchesDefinition = GenericBaseDefinitions(openDefinition) + .Any(definition => definition == fieldDefinition); + + if (!matchesDefinition) return false; + + try + { + closed = openDefinition.MakeGenericType(fieldArguments); + } + catch (Exception) + { + closed = null; + return false; + } + + return fieldType.IsAssignableFrom(closed); + } + + /// + /// Returns the explicit base-type/interface constraints of (excluding + /// other type parameters), or { typeof(object) } when it has none. Used as the base-type filter + /// for the argument's candidate list. + /// + public static Type[] GetConstraintBaseTypes(Type parameter) + { + var constraints = parameter.GetGenericParameterConstraints() + .Where(constraint => !constraint.IsGenericParameter && !constraint.ContainsGenericParameters) + .ToArray(); + + return constraints.Length > 0 ? constraints : new[] { typeof(object) }; + } + + /// + /// Returns when satisfies the special + /// (struct/class/new()) constraints declared on . + /// + public static bool SatisfiesSpecialConstraints(Type parameter, Type candidate) + { + if (candidate is null) return false; + + var special = parameter.GenericParameterAttributes & GenericParameterAttributes.SpecialConstraintMask; + var requireValueType = (special & GenericParameterAttributes.NotNullableValueTypeConstraint) != 0; + var requireReferenceType = (special & GenericParameterAttributes.ReferenceTypeConstraint) != 0; + var requireDefaultCtor = (special & GenericParameterAttributes.DefaultConstructorConstraint) != 0; + + if (requireValueType && !candidate.IsValueType) return false; + if (requireReferenceType && candidate.IsValueType) return false; + + return !requireDefaultCtor || candidate.IsValueType || candidate.GetConstructor(Type.EmptyTypes) is not null; + } + + /// + /// Closes over and validates the result + /// against every entry of . Returns with a + /// human-readable when construction throws (a violated parameter constraint) + /// or the closed type is not assignable to the field. + /// + public static bool TryConstruct(Type openDefinition, Type[] arguments, Type[] fieldTypes, out Type closed, out string error) + { + closed = null; + error = null; + + try + { + closed = openDefinition.MakeGenericType(arguments); + } + catch (Exception exception) + { + error = $"Cannot construct {FormatDefinitionName(openDefinition)}: {exception.Message}"; + return false; + } + + // The chosen arguments may satisfy the type parameters' own constraints yet still produce a type + // that is not assignable to the managed-reference field — guard against a value Unity would drop. + if (fieldTypes is not null) + { + foreach (var fieldType in fieldTypes) + { + if (fieldType is null || fieldType == typeof(object)) continue; + if (fieldType.IsAssignableFrom(closed)) continue; + + error = $"{closed.Name} is not assignable to {fieldType.Name}."; + closed = null; + return false; + } + } + + return true; + } + + /// + /// Short display form of an open definition with its parameter names (Modifier<T>). + /// + public static string FormatDefinitionName(Type definition) + { + var name = definition.Name; + var tick = name.IndexOf('`'); + var baseName = tick >= 0 ? name[..tick] : name; + var arguments = string.Join(", ", definition.GetGenericArguments().Select(argument => argument.Name)); + return $"{baseName}<{arguments}>"; + } + + private static bool CanCloseToFieldType(Type openDefinition, Type fieldType) + { + if (fieldType.IsGenericType) + { + var fieldDefinition = fieldType.GetGenericTypeDefinition(); + return GenericBaseDefinitions(openDefinition).Any(definition => definition == fieldDefinition); + } + + if (fieldType.IsAssignableFrom(openDefinition)) return true; + if (openDefinition.GetInterfaces().Contains(fieldType)) return true; + + for (var current = openDefinition.BaseType; current is not null; current = current.BaseType) + if (current == fieldType) return true; + + return false; + } + + /// + /// Enumerates the generic-type-definition view of itself, its base + /// class chain, and its interfaces (only the generic ones, reduced to their definitions). + /// + private static IEnumerable GenericBaseDefinitions(Type openDefinition) + { + if (openDefinition.IsGenericType) + yield return openDefinition.GetGenericTypeDefinition(); + + for (var current = openDefinition.BaseType; current is not null; current = current.BaseType) + if (current.IsGenericType) + yield return current.GetGenericTypeDefinition(); + + foreach (var contract in openDefinition.GetInterfaces()) + if (contract.IsGenericType) + yield return contract.GetGenericTypeDefinition(); + } + + private static IEnumerable EnumerateDomainTypes() + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + Type[] types; + + try + { + types = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + types = ex.Types.Where(type => type is not null).ToArray(); + } + + foreach (var type in types) + yield return type; + } + } + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/GenericTypeResolver.cs.meta similarity index 86% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/GenericTypeResolver.cs.meta index 4aafb67c..51af064f 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Selectors/GenericArgumentSelectorWindow.cs.meta +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/GenericTypeResolver.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: b201e7ce74c914927b40f42ff5e7f1cb +guid: 3f7c2a1d9b6e4c84a1d05f3e72c8b9a4 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs index fe3a6df8..2bae64ca 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/HierarchyBuilder.cs @@ -11,12 +11,16 @@ public static TreeNode Build( Type[] types, TypeAllow allow, Func filter = null, - IEnumerable additionalTypes = null) + IEnumerable additionalTypes = null, + bool includeNoneOption = true) { var allTypes = TypeInfo.GetAllTypeInfos(types, allow, filter, additionalTypes); var root = new TreeNode("/"); - root.Children.Add(new TreeNode(TypeSelectorHelpers.NoneOption, null, TypeSelectorHelpers.NoneOption)); + + // Generic-argument pages must yield a concrete type, so they omit the entry. + if (includeNoneOption) + root.Children.Add(new TreeNode(TypeSelectorHelpers.NoneOption, null, TypeSelectorHelpers.NoneOption)); AddGlobalNamespaceGroup(root, allTypes); AddNamespaceHierarchy(root, allTypes); diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs index f0442916..7b0266c5 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeInfo.cs @@ -78,7 +78,8 @@ public static List GetAllTypeInfos( /// /// Short display name for a type. Open generic definitions are rendered with angle-bracket - /// parameters (Modifier<T>) instead of Unity's raw arity form (Modifier`1). + /// parameters (Modifier<T>) instead of Unity's raw arity form (Modifier`1); + /// generic arguments are formatted recursively so nested closed generics render fully. /// private static string FormatName(Type type) { @@ -87,7 +88,7 @@ private static string FormatName(Type type) var name = type.Name; var tick = name.IndexOf('`'); var baseName = tick >= 0 ? name[..tick] : name; - var arguments = string.Join(", ", type.GetGenericArguments().Select(argument => argument.Name)); + var arguments = string.Join(", ", type.GetGenericArguments().Select(FormatName)); return $"{baseName}<{arguments}>"; } } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs index 7806f258..25c821a5 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/Types/Selectors/TypeSelectorWindow.cs @@ -14,6 +14,13 @@ namespace Aspid.FastTools.Types.Editors /// /// Editor window that displays a hierarchical type selector dropdown, allowing the user to browse and select a from a filtered list. /// + /// + /// Selecting an open generic definition (injected via additionalTypes) does not close the window; + /// instead it drills into an in-window argument-selection flow — one hierarchical page per type parameter, + /// reusing the same search/keyboard/navigation — and emits the constructed closed type once every argument + /// is resolved. The argument flow stays dormant unless open generics are present, so the ordinary + /// type-selection contract is unchanged. + /// public sealed class TypeSelectorWindow : EditorWindow { private const string StylesheetPath = "UI/Types/Aspid-FastTools-TypeSelectorWindow"; @@ -24,12 +31,19 @@ public sealed class TypeSelectorWindow : EditorWindow private Label _titleLabel; private Button _backButton; private ListView _listView; + private Label _errorLabel; private ToolbarSearchField _searchField; - private NavigationController _navigation; + + private readonly List _pages = new(); private Action _onSelected; + private Func _argumentFilter; + private Type[] _fieldTypes = Array.Empty(); private string _currentAqn = string.Empty; + private NavigationController Nav => _pages[^1].Navigation; + private bool CanGoBack => Nav.CanNavigateBack || _pages.Count > 1; + /// /// Opens the type selector window as a dropdown anchored to . /// @@ -37,9 +51,10 @@ public sealed class TypeSelectorWindow : EditorWindow /// Base types used to filter which concrete types are shown. Only types assignable to all entries are listed. /// Assembly-qualified name of the currently selected type, used to pre-navigate to that type's location. Pass null or empty to start at the root. /// Which type kinds are included in the list. Defaults to TypeAllow.None. - /// Callback invoked with the assembly-qualified name of the selected type, or null if the user chose <None>. + /// Callback invoked with the assembly-qualified name of the selected type, or null if the user chose <None>. When an open generic is resolved, the assembly-qualified name of the constructed closed type is passed. /// Optional predicate applied to each candidate type after the base-type and checks. Return false to hide a type. Pass null to keep every matching type. /// Optional extra types appended to the list verbatim, bypassing the base-type and checks — used to inject entries the assignability scan cannot match, such as open generic definitions. + /// Optional predicate applied to candidate types offered for an open generic's type arguments (in addition to the parameter's own constraints). Used to restrict arguments to, e.g., Unity-serializable types. Pass null to accept any constraint-satisfying type. public static void Show( Rect screenRect, Type[] types = null, @@ -47,7 +62,8 @@ public static void Show( TypeAllow allow = TypeAllow.None, Action onSelected = null, Func filter = null, - IEnumerable additionalTypes = null) + IEnumerable additionalTypes = null, + Func argumentFilter = null) { types ??= new[] { typeof(object) }; @@ -59,7 +75,8 @@ public static void Show( allow, onSelected, filter, - additionalTypes); + additionalTypes, + argumentFilter); } #region Initialization @@ -70,21 +87,36 @@ private void Initialize( TypeAllow allow, Action onSelected, Func filter, - IEnumerable additionalTypes) + IEnumerable additionalTypes, + Func argumentFilter) { _onSelected = onSelected; + _argumentFilter = argumentFilter; _currentAqn = currentAqn ?? string.Empty; + _fieldTypes = types; BuildUI(); var hierarchy = HierarchyBuilder.Build(types, allow, filter, additionalTypes); - InitializeNavigation(hierarchy, _currentAqn); + var navigation = new NavigationController(hierarchy); + + if (!string.IsNullOrWhiteSpace(_currentAqn)) + navigation.NavigateToAssemblyQualifiedName(_currentAqn); + + _pages.Add(new PickerPage + { + Navigation = navigation, + TitlePrefix = null, + ConstraintType = types.Length > 0 ? types[0] : typeof(object), + OnPicked = closed => Emit(closed?.AssemblyQualifiedName), + IsBase = true, + }); RefreshView(); var size = new Vector2(Mathf.Max(350, screenRect.width), 320); ShowAsDropDown(screenRect, size); - + _searchField.Focus(); } @@ -92,12 +124,14 @@ private void BuildUI() { _searchField = CreateSearchField(); _listView = CreateListView(); + _errorLabel = CreateErrorLabel(); rootVisualElement .AddStyleSheetsFromResource(StylesheetPath) .AddStyleSheetsFromResource(AspidStyles.DefaultStyleSheet) .AddChild(CreateHeader()) .AddChild(_searchField) + .AddChild(_errorLabel) .AddChild(_listView); rootVisualElement.RegisterCallback(HandleKeyDown, TrickleDown.TrickleDown); @@ -123,6 +157,21 @@ ToolbarSearchField CreateSearchField() return field; } + Label CreateErrorLabel() + { + var label = new Label(string.Empty); + + label.style.display = DisplayStyle.None; + label.style.color = new Color(0.9f, 0.35f, 0.35f); + label.style.whiteSpace = WhiteSpace.Normal; + label.style.marginLeft = 4; + label.style.marginRight = 4; + label.style.marginTop = 2; + label.style.marginBottom = 2; + + return label; + } + ListView CreateListView() { var list = new ListView @@ -153,7 +202,7 @@ VisualElement CreateListItem() void BindListItem(VisualElement element, int index) { - var items = _navigation?.CurrentItems; + var items = _pages.Count > 0 ? Nav.CurrentItems : null; if (items is null) return; if (index < 0 || index >= items.Count) return; @@ -164,19 +213,11 @@ void BindListItem(VisualElement element, int index) .SetTooltip(node.Tooltip); element.Q