diff --git a/Assets/Tests/InputSystem.Editor/CustomProcessorEnumTest.cs b/Assets/Tests/InputSystem.Editor/CustomProcessorEnumTest.cs new file mode 100644 index 0000000000..f2e9cb174a --- /dev/null +++ b/Assets/Tests/InputSystem.Editor/CustomProcessorEnumTest.cs @@ -0,0 +1,112 @@ +#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS && UNITY_6000_0_OR_NEWER + +using System; +using NUnit.Framework; +using System.Collections; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.Editor; +using UnityEngine.TestTools; +using UnityEngine.UIElements; + +internal enum SomeEnum +{ + OptionA = 10, + OptionB = 20 +} + +#if UNITY_EDITOR +[InitializeOnLoad] +#endif +internal class CustomProcessor : InputProcessor<float> +{ + public SomeEnum SomeEnum; + +#if UNITY_EDITOR + static CustomProcessor() + { + Initialize(); + } + +#endif + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Initialize() + { + InputSystem.RegisterProcessor<CustomProcessor>(); + } + + public override float Process(float value, InputControl control) + { + return value; + } +} + +internal class CustomProcessorEnumTest : UIToolkitBaseTestWindow<InputActionsEditorWindow> +{ + InputActionAsset m_Asset; + + public override void OneTimeSetUp() + { + base.OneTimeSetUp(); + m_Asset = AssetDatabaseUtils.CreateAsset<InputActionAsset>(); + + var actionMap = m_Asset.AddActionMap("Action Map"); + + actionMap.AddAction("Action", InputActionType.Value, processors: "Custom(SomeEnum=10)"); + } + + public override void OneTimeTearDown() + { + AssetDatabaseUtils.Restore(); + base.OneTimeTearDown(); + } + + public override IEnumerator UnitySetup() + { + m_Window = InputActionsEditorWindow.OpenEditor(m_Asset); + yield return null; + } + + [UnityTest] + public IEnumerator ProcessorEnum_ShouldSerializeByValue_WhenSerializedToAsset() + { + // Serialize current asset to JSON, and check that initial JSON contains default enum value for OptionA + var json = m_Window.currentAssetInEditor.ToJson(); + + Assert.That(json.Contains("Custom(SomeEnum=10)"), Is.True, + "Serialized JSON does not contain the expected custom processor string for OptionA."); + + // Query the dropdown with exactly two enum choices and check that the drop down is present in the UI + var dropdownList = m_Window.rootVisualElement.Query<DropdownField>().Where(d => d.choices.Count == 2).ToList(); + Assume.That(dropdownList.Count > 0, Is.True, "Enum parameter dropdown not found in the UI."); + + // Determine the new value to be set in the dropdown, focus the dropdown before dispatching the change + var dropdown = dropdownList.First(); + var newValue = dropdown.choices[1]; + dropdown.Focus(); + dropdown.value = newValue; + + // Create and send a change event from OptionA to OptionB + var changeEvent = ChangeEvent<Enum>.GetPooled(SomeEnum.OptionA, SomeEnum.OptionB); + changeEvent.target = dropdown; + dropdown.SendEvent(changeEvent); + + // Find the save button in the window, focus and click the save button to persist the changes + var saveButton = m_Window.rootVisualElement.Q<Button>("save-asset-toolbar-button"); + Assume.That(saveButton, Is.Not.Null, "Save Asset button not found in the UI."); + saveButton.Focus(); + SimulateClickOn(saveButton); + + Assert.That(dropdown.value, Is.EqualTo(newValue)); + + // Verify that the updated JSON contains the new enum value for OpitonB + var updatedJson = m_Window.currentAssetInEditor.ToJson(); + Assert.That(updatedJson.Contains("Custom(SomeEnum=20)"), Is.True, "Serialized JSON does not contain the updated custom processor string for OptionB."); + + yield return null; + } +} +#endif diff --git a/Assets/Tests/InputSystem.Editor/CustomProcessorEnumTest.cs.meta b/Assets/Tests/InputSystem.Editor/CustomProcessorEnumTest.cs.meta new file mode 100644 index 0000000000..bb391f4d03 --- /dev/null +++ b/Assets/Tests/InputSystem.Editor/CustomProcessorEnumTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2ad69ed3d3bf6e343939dbdf0d95803b \ No newline at end of file diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index 37be511d5b..0a4d551fdd 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -5215,8 +5215,8 @@ public void Actions_CanConvertAssetToAndFromJson() static string MinimalJson(string name = null) { if (name != null) - return "{\n \"name\": \"" + name + "\",\n \"maps\": [],\n \"controlSchemes\": []\n}"; - return "{\n \"maps\": [],\n \"controlSchemes\": []\n}"; + return "{\n \"version\": 0,\n \"name\": \"" + name + "\",\n \"maps\": [],\n \"controlSchemes\": []\n}"; + return "{\n \"version\": 0,\n \"maps\": [],\n \"controlSchemes\": []\n}"; } [Test] diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 6425648904..e9e79a41f1 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -38,6 +38,7 @@ however, it has to be formatted properly to pass verification tests. - Fixed the defaultActionMap dropdown in the PlayerInput component defaulting to <None> instead of the first ActionMap. - Fixed TrackedPoseDriver stops updating position and rotation when device is added after its initialization. [ISXB-1555](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1555) - Fixed PlayerInput component not working with C# Wrappers (ISXB-1535). This reverted changes done to fix [ISXB-920](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-920) but users can now fix it themselves. +- Fixed an issue that caused input processors with enum properties to incorrectly serialise by index instead of by value [ISXB-1474](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1474) ## [1.14.0] - 2025-03-20 diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs index 7ddd84661e..3b9a70fcca 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs @@ -1,6 +1,9 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine.InputSystem.Editor; using UnityEngine.InputSystem.Utilities; ////TODO: make the FindAction logic available on any IEnumerable<InputAction> and IInputActionCollection via extension methods @@ -275,6 +278,21 @@ public InputAction this[string actionNameOrId] return action; } } + /// <summary> + /// File‐format version constants for InputActionAsset JSON. + /// </summary> + static class JsonVersion + { + /// <summary>The original JSON version format for InputActionAsset.</summary> + public const int Version0 = 0; + + /// <summary>Updated JSON version format for InputActionAsset.</summary> + /// <remarks>Changes representation of parameter values from being serialized by value to being serialized by value.</remarks> + public const int Version1 = 1; + + /// <summary>The current version.</summary> + public const int Current = Version1; + } /// <summary> /// Return a JSON representation of the asset. @@ -296,8 +314,10 @@ public InputAction this[string actionNameOrId] /// <seealso cref="FromJson"/> public string ToJson() { + var hasContent = m_ActionMaps.LengthSafe() > 0 || m_ControlSchemes.LengthSafe() > 0; return JsonUtility.ToJson(new WriteFileJson { + version = hasContent ? JsonVersion.Current : JsonVersion.Version0, name = name, maps = InputActionMap.WriteFileJson.FromMaps(m_ActionMaps).maps, controlSchemes = InputControlScheme.SchemeJson.ToJson(m_ControlSchemes), @@ -379,6 +399,7 @@ public void LoadFromJson(string json) throw new ArgumentNullException(nameof(json)); var parsedJson = JsonUtility.FromJson<ReadFileJson>(json); + MigrateJson(ref parsedJson); parsedJson.ToAsset(this); } @@ -950,6 +971,7 @@ private void OnDestroy() [Serializable] internal struct WriteFileJson { + public int version; public string name; public InputActionMap.WriteMapJson[] maps; public InputControlScheme.SchemeJson[] controlSchemes; @@ -965,6 +987,7 @@ internal struct WriteFileJsonNoName [Serializable] internal struct ReadFileJson { + public int version; public string name; public InputActionMap.ReadMapJson[] maps; public InputControlScheme.SchemeJson[] controlSchemes; @@ -981,5 +1004,73 @@ public void ToAsset(InputActionAsset asset) map.m_Asset = asset; } } + + /// <summary> + /// If parsedJson.version is older than Current, rewrite every + /// action.processors entry to replace “enumName(Ordinal=…)” with + /// “enumName(Value=…)” and bump parsedJson.version. + /// </summary> + internal void MigrateJson(ref ReadFileJson parsedJson) + { + if (parsedJson.version >= JsonVersion.Version1) + return; + if ((parsedJson.maps?.Length ?? 0) > 0 && (parsedJson.version) < JsonVersion.Version1) + { + for (var mi = 0; mi < parsedJson.maps.Length; ++mi) + { + var mapJson = parsedJson.maps[mi]; + for (var ai = 0; ai < mapJson.actions.Length; ++ai) + { + var actionJson = mapJson.actions[ai]; + var raw = actionJson.processors; + if (string.IsNullOrEmpty(raw)) + continue; + + var list = NameAndParameters.ParseMultiple(raw).ToList(); + var rebuilt = new List<string>(list.Count); + foreach (var nap in list) + { + var procType = InputSystem.TryGetProcessor(nap.name); + if (nap.parameters.Count == 0 || procType == null) + { + rebuilt.Add(nap.ToString()); + continue; + } + + var dict = nap.parameters.ToDictionary(p => p.name, p => p.value.ToString()); + var anyChanged = false; + foreach (var field in procType.GetFields(BindingFlags.Public | BindingFlags.Instance).Where(f => f.FieldType.IsEnum)) + { + if (dict.TryGetValue(field.Name, out var ordS) && int.TryParse(ordS, out var ord)) + { + var values = Enum.GetValues(field.FieldType).Cast<object>().ToArray(); + if (ord >= 0 && ord < values.Length) + { + dict[field.Name] = Convert.ToInt32(values[ord]).ToString(); + anyChanged = true; + } + } + } + + if (!anyChanged) + { + rebuilt.Add(nap.ToString()); + } + else + { + var paramText = string.Join(",", dict.Select(kv => $"{kv.Key}={kv.Value}")); + rebuilt.Add($"{nap.name}({paramText})"); + } + } + + actionJson.processors = string.Join(";", rebuilt); + mapJson.actions[ai] = actionJson; + } + parsedJson.maps[mi] = mapJson; + } + } + // Bump the version so we never re-migrate + parsedJson.version = JsonVersion.Version1; + } } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/ParameterListView.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/ParameterListView.cs index 4e1df4bb84..2828d7654a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/ParameterListView.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/ParameterListView.cs @@ -278,11 +278,26 @@ void OnEditEnd() if (parameter.isEnum) { - var intValue = parameter.value.value.ToInt32(); - var field = new DropdownField(label.text, parameter.enumNames.Select(x => x.text).ToList(), intValue); - field.tooltip = label.tooltip; - field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, field.index, closedIndex)); - field.RegisterCallback<BlurEvent>(_ => OnEditEnd()); + var names = parameter.enumNames.Select(c => c.text).ToList(); + var rawValue = parameter.value.value.ToInt32(); + var selectedIndex = parameter.enumValues.IndexOf(rawValue); + if (selectedIndex < 0 || selectedIndex >= names.Count) + selectedIndex = 0; + + var field = new DropdownField(label.text, names, selectedIndex) + { + tooltip = label.tooltip + }; + + field.RegisterValueChangedCallback(evt => + { + var newBackingValue = parameter.enumValues[field.index]; + parameter.value.value = PrimitiveValue.FromObject(newBackingValue).ConvertTo(parameter.value.type); + m_Parameters[closedIndex] = parameter; + onChange?.Invoke(); + }); + + field.RegisterCallback<BlurEvent>(_ => onChange?.Invoke()); root.Add(field); } else if (parameter.value.type == TypeCode.Int64 || parameter.value.type == TypeCode.UInt64) diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs index 03910a9d1a..ddc2ef6430 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs @@ -28,7 +28,7 @@ namespace UnityEngine.InputSystem.Editor [ScriptedImporter(kVersion, InputActionAsset.Extension)] internal class InputActionImporter : ScriptedImporter { - private const int kVersion = 13; + private const int kVersion = 14; [SerializeField] private bool m_GenerateWrapperCode; [SerializeField] private string m_WrapperCodePath; @@ -66,7 +66,6 @@ private static InputActionAsset CreateFromJson(AssetImportContext context) { // Attempt to parse JSON asset.LoadFromJson(content); - // Make sure action map names are unique within JSON file var names = new HashSet<string>(); foreach (var map in asset.actionMaps) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/DefaultInputActions.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/DefaultInputActions.cs index 9206101d9c..6c8dea7ae7 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/DefaultInputActions.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/DefaultInputActions.cs @@ -42,6 +42,7 @@ public class DefaultInputActions : IInputActionCollection2, IDisposable public @DefaultInputActions() { asset = InputActionAsset.FromJson(@"{ + ""version"": 1, ""name"": ""DefaultInputActions"", ""maps"": [ { diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/DefaultInputActions.inputactions b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/DefaultInputActions.inputactions index 6fa20869f1..45f6cc2b89 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/DefaultInputActions.inputactions +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/DefaultInputActions.inputactions @@ -1,4 +1,5 @@ { + "version": 1, "name": "DefaultInputActions", "maps": [ {