#if UNITY_EDITOR using System; using System.IO; using System.Linq; using UnityEditor; #if UNITY_2020_2_OR_NEWER using UnityEditor.AssetImporters; #else using UnityEditor.Experimental.AssetImporters; #endif using UnityEngine.InputSystem.Utilities; ////FIXME: The importer accesses icons through the asset db (which EditorGUIUtility.LoadIcon falls back on) which will //// not yet have been imported when the project is imported from scratch; this results in errors in the log and in generic //// icons showing up for the assets #pragma warning disable 0649 namespace UnityEngine.InputSystem.Editor { /// /// Imports an from JSON. /// /// /// Can generate code wrappers for the contained action sets as a convenience. /// Will not overwrite existing wrappers except if the generated code actually differs. /// [ScriptedImporter(kVersion, InputActionAsset.Extension)] internal class InputActionImporter : ScriptedImporter { private const int kVersion = 13; private const string kActionIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputAction.png"; private const string kAssetIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputActionAsset.png"; [SerializeField] private bool m_GenerateWrapperCode; [SerializeField] private string m_WrapperCodePath; [SerializeField] private string m_WrapperClassName; [SerializeField] private string m_WrapperCodeNamespace; private static InlinedArray s_OnImportCallbacks; public static event Action onImport { add => s_OnImportCallbacks.Append(value); remove => s_OnImportCallbacks.Remove(value); } public override void OnImportAsset(AssetImportContext ctx) { if (ctx == null) throw new ArgumentNullException(nameof(ctx)); foreach (var callback in s_OnImportCallbacks) callback(); ////REVIEW: need to check with version control here? // Read file. string text; try { text = File.ReadAllText(ctx.assetPath); } catch (Exception exception) { ctx.LogImportError($"Could not read file '{ctx.assetPath}' ({exception})"); return; } // Create asset. var asset = ScriptableObject.CreateInstance(); // Parse JSON. try { ////TODO: make sure action names are unique asset.LoadFromJson(text); } catch (Exception exception) { ctx.LogImportError($"Could not parse input actions in JSON format from '{ctx.assetPath}' ({exception})"); DestroyImmediate(asset); return; } // Force name of asset to be that on the file on disk instead of what may be serialized // as the 'name' property in JSON. asset.name = Path.GetFileNameWithoutExtension(assetPath); // Load icons. ////REVIEW: the icons won't change if the user changes skin; not sure it makes sense to differentiate here var assetIcon = (Texture2D)EditorGUIUtility.Load(kAssetIcon); var actionIcon = (Texture2D)EditorGUIUtility.Load(kActionIcon); // Add asset. ctx.AddObjectToAsset("", asset, assetIcon); ctx.SetMainObject(asset); // Make sure all the elements in the asset have GUIDs and that they are indeed unique. var maps = asset.actionMaps; foreach (var map in maps) { // Make sure action map has GUID. if (string.IsNullOrEmpty(map.m_Id) || asset.actionMaps.Count(x => x.m_Id == map.m_Id) > 1) map.GenerateId(); // Make sure all actions have GUIDs. foreach (var action in map.actions) { var actionId = action.m_Id; if (string.IsNullOrEmpty(actionId) || asset.actionMaps.Sum(m => m.actions.Count(a => a.m_Id == actionId)) > 1) action.GenerateId(); } // Make sure all bindings have GUIDs. for (var i = 0; i < map.m_Bindings.LengthSafe(); ++i) { var bindingId = map.m_Bindings[i].m_Id; if (string.IsNullOrEmpty(bindingId) || asset.actionMaps.Sum(m => m.bindings.Count(b => b.m_Id == bindingId)) > 1) map.m_Bindings[i].GenerateId(); } } // Create subasset for each action. foreach (var map in maps) { foreach (var action in map.actions) { var actionReference = ScriptableObject.CreateInstance(); actionReference.Set(action); ctx.AddObjectToAsset(action.m_Id, actionReference, actionIcon); // Backwards-compatibility (added for 1.0.0-preview.7). // We used to call AddObjectToAsset using objectName instead of action.m_Id as the object name. This fed // the action name (*and* map name) into the hash generation that was used as the basis for the file ID // object the InputActionReference object. Thus, if the map and/or action name changed, the file ID would // change and existing references to the InputActionReference object would become invalid. // // What we do here is add another *hidden* InputActionReference object with the same content to the // asset. This one will use the old file ID and thus preserve backwards-compatibility. We should be able // to remove this for 2.0. // // Case: https://fogbugz.unity3d.com/f/cases/1229145/ var backcompatActionReference = Instantiate(actionReference); backcompatActionReference.name = actionReference.name; // Get rid of the (Clone) suffix. backcompatActionReference.hideFlags = HideFlags.HideInHierarchy; ctx.AddObjectToAsset(actionReference.name, backcompatActionReference, actionIcon); } } // Generate wrapper code, if enabled. if (m_GenerateWrapperCode) { // When using code generation, it is an error for any action map to be named the same as the asset itself. // https://fogbugz.unity3d.com/f/cases/1212052/ var className = !string.IsNullOrEmpty(m_WrapperClassName) ? m_WrapperClassName : CSharpCodeHelpers.MakeTypeName(asset.name); if (maps.Any(x => CSharpCodeHelpers.MakeTypeName(x.name) == className || CSharpCodeHelpers.MakeIdentifier(x.name) == className)) { ctx.LogImportError( $"{asset.name}: An action map in an .inputactions asset cannot be named the same as the asset itself if 'Generate C# Class' is used. " + "You can rename the action map in the asset, rename the asset itself or assign a different C# class name in the import settings."); } else { var wrapperFilePath = m_WrapperCodePath; if (string.IsNullOrEmpty(wrapperFilePath)) { // Placed next to .inputactions file. var assetPath = ctx.assetPath; var directory = Path.GetDirectoryName(assetPath); var fileName = Path.GetFileNameWithoutExtension(assetPath); wrapperFilePath = Path.Combine(directory, fileName) + ".cs"; } else if (wrapperFilePath.StartsWith("./") || wrapperFilePath.StartsWith(".\\") || wrapperFilePath.StartsWith("../") || wrapperFilePath.StartsWith("..\\")) { // User-specified file relative to location of .inputactions file. var assetPath = ctx.assetPath; var directory = Path.GetDirectoryName(assetPath); wrapperFilePath = Path.Combine(directory, wrapperFilePath); } else if (!wrapperFilePath.ToLower().StartsWith("assets/") && !wrapperFilePath.ToLower().StartsWith("assets\\")) { // User-specified file in Assets/ folder. wrapperFilePath = Path.Combine("Assets", wrapperFilePath); } var dir = Path.GetDirectoryName(wrapperFilePath); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); var options = new InputActionCodeGenerator.Options { sourceAssetPath = ctx.assetPath, namespaceName = m_WrapperCodeNamespace, className = m_WrapperClassName, }; if (InputActionCodeGenerator.GenerateWrapperCode(wrapperFilePath, asset, options)) { // When we generate the wrapper code cs file during asset import, we cannot call ImportAsset on that directly because // script assets have to be imported before all other assets, and are not allowed to be added to the import queue during // asset import. So instead we register a callback to trigger a delayed asset refresh which should then pick up the // changed/added script, and trigger a new import. EditorApplication.delayCall += AssetDatabase.Refresh; } } } // Refresh editors. InputActionEditorWindow.RefreshAllOnAssetReimport(); } ////REVIEW: actually pre-populate with some stuff? private const string kDefaultAssetLayout = "{}"; // Add item to plop an .inputactions asset into the project. [MenuItem("Assets/Create/Input Actions")] public static void CreateInputAsset() { ProjectWindowUtil.CreateAssetWithContent("New Controls." + InputActionAsset.Extension, kDefaultAssetLayout, (Texture2D)EditorGUIUtility.Load(kAssetIcon)); } } } #endif // UNITY_EDITOR