From 4ccc78fe539ccd13c73522215496cb5a9ed4295c Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:08:54 +0900 Subject: [PATCH 1/5] feat(editor): support Unity 7000 CoreCLR assembly discovery Why CoreCLR may retain assemblies from unloading code contexts in AppDomain.GetAssemblies after code reload. What Changed Use Unity's CurrentAssemblies API on Unity 7000 and newer when discovering generic type candidates. Keep the version-specific implementation behind a dedicated method. Impact Candidates come from Unity's current loaded assemblies on CoreCLR. Earlier Unity versions retain their existing discovery and filtering behavior. --- .../Unity_2023_2_OrNewer_TypeCandiateProvider.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateProvider/Unity_2023_2_OrNewer_TypeCandiateProvider.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateProvider/Unity_2023_2_OrNewer_TypeCandiateProvider.cs index 4ed3c3c..136f5f4 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateProvider/Unity_2023_2_OrNewer_TypeCandiateProvider.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateProvider/Unity_2023_2_OrNewer_TypeCandiateProvider.cs @@ -73,7 +73,7 @@ private IEnumerable GetTypesWithGeneric (Type baseType) private static IEnumerable EnumerateAllTypesSafely () { - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + foreach (var asm in GetLoadedAssemblies()) { Type[] types; try @@ -99,6 +99,16 @@ private static IEnumerable EnumerateAllTypesSafely () } } } + +#if UNITY_7000_0_OR_NEWER + // CoreCLR can retain assemblies from unloading code contexts. Unity's + // assembly list excludes them so candidates belong to the current code. + private static IEnumerable GetLoadedAssemblies () + => UnityEngine.Assemblies.CurrentAssemblies.GetLoadedAssemblies(); +#else + private static IEnumerable GetLoadedAssemblies () + => AppDomain.CurrentDomain.GetAssemblies(); +#endif } } #endif From 83d3b01858552ca956ccfe2b3f912927a1f19666 Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:08:55 +0900 Subject: [PATCH 2/5] ci(unity): cover Unity 7000 CoreCLR in the EditMode matrix Why The CoreCLR assembly discovery path needs Linux coverage alongside the minimum and current Mono-based Unity versions. What Changed Add Unity 7000.0.0a11 to the existing EditMode matrix. Impact The Verification check now requires the Unity 7000 job as well as the existing 2021.3 and Unity 6 jobs. Release builds keep their existing Editor version. --- .github/workflows/tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0313bea..b88a9f4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -50,6 +50,7 @@ jobs: unityVersion: - 2021.3.45f2 - 6000.6.6f1 + - 7000.0.0a11 steps: - name: Checkout From 7e6c47d349e245af4f0f9ac3d556c2c3e40aa6ea Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:35:23 +0900 Subject: [PATCH 3/5] fix(editor): resolve CoreCLR nested serialized type names Why Unity 7000 serializes nested managed type names with slash separators. Direct reflection lookup cannot resolve them, leaving IMGUI labels empty and preventing the type popup from opening. What Changed Retry nested-name resolution using the same slash-to-plus conversion as Unity's ScriptAttributeUtility. Keep the conversion in ManagedReferenceUtility instead of requiring individual callers to normalize names. Impact Both Inspector backends and menu commands share the same type-name interpretation. The 51 affected type-selection, drawer, change-check and menu tests pass on Unity 7000 locally. --- .../Editor/ManagedReferenceContextualPropertyMenu.cs | 2 +- .../Editor/ManagedReferenceSelection.cs | 2 +- .../Editor/ManagedReferenceUtility.cs | 10 +++++++++- .../Editor/SubclassSelectorField.cs | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceContextualPropertyMenu.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceContextualPropertyMenu.cs index ca8b811..ddc4559 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceContextualPropertyMenu.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceContextualPropertyMenu.cs @@ -215,7 +215,7 @@ private static bool CanPasteTypeAndValues (SerializedProperty property, Clipboar } // Unity uses '/' for nested types in its static field type name; reflection uses '+'. - Type fieldType = ManagedReferenceUtility.GetType(property.managedReferenceFieldTypename.Replace('/', '+')); + Type fieldType = ManagedReferenceUtility.GetType(property.managedReferenceFieldTypename); return fieldType != null && fieldType.IsAssignableFrom(copiedType); } diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceSelection.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceSelection.cs index b9ead75..27b2583 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceSelection.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceSelection.cs @@ -11,7 +11,7 @@ internal static bool SelectType (DeferredManagedReferenceTarget target, Type typ { for (int i = 0; i < properties.Length; i++) { - Type fieldType = ManagedReferenceUtility.GetType(properties[i].managedReferenceFieldTypename.Replace('/', '+')); + Type fieldType = ManagedReferenceUtility.GetType(properties[i].managedReferenceFieldTypename); if (!properties[i].editable || (type != null && (fieldType == null || !fieldType.IsAssignableFrom(type)))) { return false; diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceUtility.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceUtility.cs index b435e13..08a102a 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceUtility.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/ManagedReferenceUtility.cs @@ -80,7 +80,15 @@ public static Type GetType (string typeName) int splitIndex = typeName.IndexOf(' '); var assembly = Assembly.Load(typeName.Substring(0, splitIndex)); - return assembly.GetType(typeName.Substring(splitIndex + 1)); + string className = typeName.Substring(splitIndex + 1); + Type type = assembly.GetType(className); + // CoreCLR's serialized nested names use '/', while reflection uses '+'. + // Resolve them the same way as Unity's ScriptAttributeUtility. + if (type == null && className.IndexOf('/') >= 0) + { + type = assembly.GetType(className.Replace('/', '+')); + } + return type; } } diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorField.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorField.cs index f393838..33a482b 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorField.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorField.cs @@ -420,7 +420,7 @@ private void ShowTypePopup () { return; } - Type baseType = ManagedReferenceUtility.GetType(fieldTypeName.Replace('/', '+')); + Type baseType = ManagedReferenceUtility.GetType(fieldTypeName); var types = TypeSearchService.TypeCandiateService.GetDisplayableTypes(baseType); if (typePopup == null) { From 403408549a193095700c431cd082a823b30a1282 Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:35:24 +0900 Subject: [PATCH 4/5] ci(unity): allow targeted manual regression runs Why Investigating a Unity 7000 failure should not repeat unaffected Editor versions or unrelated tests. What Changed Allow workflow dispatches to select an Editor version and test filter. Name the resulting gate Targeted verification so a diagnostic run cannot satisfy the full Verification requirement. Impact Pull requests and main still run the full required matrix. Manual diagnostics can verify the affected runtime and operations independently. --- .github/workflows/tests.yaml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b88a9f4..d7a8bc3 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -5,7 +5,16 @@ on: branches: - main pull_request: {} - workflow_dispatch: {} + workflow_dispatch: + inputs: + unityVersion: + description: 'Editor version for a targeted run (empty runs the full matrix)' + type: string + required: false + testFilter: + description: 'Unity test filter for a targeted run (empty runs all tests)' + type: string + required: false permissions: contents: read @@ -47,10 +56,7 @@ jobs: strategy: fail-fast: false matrix: - unityVersion: - - 2021.3.45f2 - - 6000.6.6f1 - - 7000.0.0a11 + unityVersion: ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.unityVersion != '' && format('["{0}"]', inputs.unityVersion) || '["2021.3.45f2","6000.6.6f1","7000.0.0a11"]') }} steps: - name: Checkout @@ -81,6 +87,7 @@ jobs: env: LIBGL_ALWAYS_SOFTWARE: '1' UNITY_EDITOR_VERSION: ${{ matrix.unityVersion }} + UNITY_TEST_FILTER: ${{ github.event_name == 'workflow_dispatch' && inputs.testFilter || '' }} run: | mkdir -p Logs xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" \ @@ -90,11 +97,16 @@ jobs: window_manager_pid=$! trap 'kill "$window_manager_pid" 2>/dev/null || true' EXIT timeout 15 bash -c 'until xprop -root _NET_SUPPORTING_WM_CHECK | grep -q "window id"; do sleep 0.1; done' + test_arguments=() + if [[ -n "$UNITY_TEST_FILTER" ]]; then + test_arguments+=(--filter "$UNITY_TEST_FILTER") + fi unity --non-interactive test . \ --editor-version "$UNITY_EDITOR_VERSION" \ --mode EditMode \ --output Logs/TestResults/editmode.xml \ --timeout 1200 \ + "${test_arguments[@]}" \ -- -force-glcore -timestamps -logFile Logs/editmode.log DISPLAY_SESSION @@ -115,7 +127,7 @@ jobs: path: Logs/ verify: - name: Verification + name: ${{ github.event_name == 'workflow_dispatch' && (inputs.unityVersion != '' || inputs.testFilter != '') && 'Targeted verification' || 'Verification' }} needs: [changes, test] if: always() runs-on: ubuntu-22.04 From 701eeaac17448cc6ee534b885996969f3dc032c6 Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:52:17 +0900 Subject: [PATCH 5/5] fix(uitoolkit): preserve the native selector header hierarchy Why Unity 7 applies list-row margins to BaseFields directly under PropertyField. An extra selector wrapper prevented those native rules from reaching the popup, making it too wide. What Changed Make the header itself a native PopupField. Place the existing Foldout prefix in a sibling flow slot so it keeps its own native margins and logical Foldout ownership. Detach and hide both owned elements together. Impact Unity calculates input widths and prefix indentation without measured offsets or layout corrections. The affected Unity 7000 checks for native geometry, resizing, focus, foldout interaction and Prefab revert pass. --- .../Editor/SubclassSelectorField.cs | 17 ++++++--- .../Editor/SubclassSelectorHeader.cs | 35 ++++++++++--------- .../Editor/SubclassSelectorTypeField.cs | 2 +- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorField.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorField.cs index 33a482b..b7c36ff 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorField.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorField.cs @@ -96,6 +96,7 @@ private void OnDetach (DetachFromPanelEvent evt) if (nativeHeader != null) { nativeHeader.style.display = DisplayStyle.None; + nativeHeader.PrefixSlot.style.display = DisplayStyle.None; } // A parent can be traversing its children during Detach. Release the // owned header after that traversal, unless this view is reattached. @@ -108,6 +109,7 @@ private void ReleaseHeader () if (nativeHeader != null && (panel == null || nativeHeader.Host != GetFirstAncestorOfType())) { hierarchy.Insert(0, header); + nativeHeader.PrefixSlot.RemoveFromHierarchy(); nativeHeader.RemoveFromHierarchy(); nativeHeader = null; } @@ -128,6 +130,7 @@ private void EnsureHeader () } if (nativeHeader?.Host != host) { + nativeHeader?.PrefixSlot.RemoveFromHierarchy(); nativeHeader?.RemoveFromHierarchy(); nativeHeader = new SubclassSelectorHeader(host, header, ShowTypePopup, HasCurrentMenuProperty); } @@ -144,11 +147,17 @@ private void EnsureHeader () nativeHeader.RemoveFromHierarchy(); container.hierarchy.Insert(container.hierarchy.IndexOf(this), nativeHeader); } - if (header.hierarchy.parent != nativeHeader) + if (nativeHeader.PrefixSlot.hierarchy.parent != container + || container.hierarchy.IndexOf(nativeHeader.PrefixSlot) != container.hierarchy.IndexOf(nativeHeader) - 1) + { + nativeHeader.PrefixSlot.RemoveFromHierarchy(); + container.hierarchy.Insert(container.hierarchy.IndexOf(nativeHeader), nativeHeader.PrefixSlot); + } + if (header.hierarchy.parent != nativeHeader.PrefixSlot) { // Native Add preserves Foldout as logical owner (including Simple // ListView's direct-child selectors) while declaring its prefix slot. - headerSlot = nativeHeader; + headerSlot = nativeHeader.PrefixSlot; try { Insert(0, header); @@ -405,7 +414,7 @@ private void RefreshDefaultBody () private void ShowTypePopup () { if (panel == null || serializedObject.targetObject == null || !enabledInHierarchy - || nativeHeader == null || !nativeHeader.TypeField.enabledInHierarchy) + || nativeHeader == null || !nativeHeader.enabledInHierarchy) { return; } @@ -430,7 +439,7 @@ private void ShowTypePopup () { typePopup.SetTypes(types); } - typePopup.ShowForSelection(nativeHeader.TypeField.Input.worldBound, item => SelectType(target, item.Type)); + typePopup.ShowForSelection(nativeHeader.Input.worldBound, item => SelectType(target, item.Type)); } private void SelectType (DeferredManagedReferenceTarget target, Type type) diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorHeader.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorHeader.cs index 0542915..2308bbe 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorHeader.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorHeader.cs @@ -9,7 +9,7 @@ namespace MackySoft.SerializeReferenceExtensions.Editor { // PopupField owns the line's flow and native input column. Foldout's actual // Toggle owns the prefix; neither control supplies measurements to the other. - internal sealed class SubclassSelectorHeader : VisualElement + internal sealed class SubclassSelectorHeader : SubclassSelectorTypeField { private static readonly Action UpdatePrefabStyle = BindPrefabStyle(); private static readonly Action UpdateInspectorStyle = BindInspectorStyle(); @@ -18,23 +18,24 @@ internal sealed class SubclassSelectorHeader : VisualElement private readonly VisualElement checkmark; internal PropertyField Host { get; } - internal SubclassSelectorTypeField TypeField { get; } + internal VisualElement PrefixSlot { get; } internal SubclassSelectorHeader (PropertyField host, Toggle toggle, Action showPopup, Func hasCurrentProperty) + : base(showPopup) { Host = host; this.toggle = toggle; + // Keep the native Foldout prefix outside PopupField's own margins. + // Its absolute Toggle consumes no flow height; the PopupField remains + // a direct child of PropertyField for Unity's list-row selectors. + PrefixSlot = new VisualElement { pickingMode = PickingMode.Ignore }; toggle.showMixedValue = false; checkmark = toggle.Q(className: Toggle.checkmarkUssClassName); - pickingMode = PickingMode.Ignore; - style.justifyContent = Justify.Center; - TypeField = new SubclassSelectorTypeField(showPopup); - TypeField.SetValueWithoutNotify(TypeMenuUtility.NullDisplayName); + SetValueWithoutNotify(TypeMenuUtility.NullDisplayName); // The popup's real label region still participates in native alignment. // The Foldout Toggle renders the visible prefix, including its arrow. - TypeField.labelElement.style.visibility = Visibility.Hidden; - TypeField.Input.AddManipulator(new FoldoutPropertyMenuManipulator(toggle, hasCurrentProperty)); - Add(TypeField); + labelElement.style.visibility = Visibility.Hidden; + Input.AddManipulator(new FoldoutPropertyMenuManipulator(toggle, hasCurrentProperty)); toggle.style.position = Position.Absolute; toggle.style.left = 0; toggle.style.right = 0; @@ -45,24 +46,26 @@ internal SubclassSelectorHeader (PropertyField host, Toggle toggle, Action showP internal void Refresh (SerializedProperty property, string label, bool mixedTypes, bool enabled, string tooltip) { - TypeField.label = label; - TypeField.tooltip = tooltip; + PrefixSlot.style.display = style.display; + PrefixSlot.visible = visible; + this.label = label; + this.tooltip = tooltip; toggle.tooltip = tooltip; - TypeField.SetEnabled(enabled); + SetEnabled(enabled); toggle.SetEnabled(enabled); // Foldout's bool is expansion state. Mixed serialized child values must // not replace its label or checkmark with Toggle's mixed-value content. toggle.showMixedValue = false; checkmark.style.visibility = mixedTypes || string.IsNullOrEmpty(property.managedReferenceFullTypename) ? Visibility.Hidden : Visibility.Visible; - bool wasMixed = TypeField.showMixedValue; - TypeField.showMixedValue = mixedTypes; + bool wasMixed = showMixedValue; + showMixedValue = mixedTypes; string displayName = TypeMenuUtility.GetTypeDisplayName(property.managedReferenceValue?.GetType()); // Clearing native mixed presentation does not restore its text. Recent // PopupFields retain the value, so the transition also needs a refresh. - if (!mixedTypes && (wasMixed || TypeField.value != displayName)) + if (!mixedTypes && (wasMixed || value != displayName)) { - TypeField.SetValueWithoutNotify(displayName); + SetValueWithoutNotify(displayName); } // Older bindings discover a Foldout header with a physical Q. // This header is still the bound Toggle, but now occupies the prefix slot. diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorTypeField.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorTypeField.cs index 1af2d69..61390b0 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorTypeField.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorTypeField.cs @@ -6,7 +6,7 @@ namespace MackySoft.SerializeReferenceExtensions.Editor { // Keep the native PopupField hierarchy and theme; replace only menu activation. - internal sealed class SubclassSelectorTypeField : PopupField + internal class SubclassSelectorTypeField : PopupField { private readonly Action showPopup;