1077 lines
49 KiB
C#
1077 lines
49 KiB
C#
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
|
||
|
||
////TODO: control schemes, like actions and maps, should have stable IDs so that they can be renamed
|
||
|
||
////REVIEW: have some way of expressing 'contracts' on action maps? I.e. something like
|
||
//// "I expect a 'look' and a 'move' action in here"
|
||
|
||
////REVIEW: rename this from "InputActionAsset" to something else that emphasizes the asset aspect less
|
||
//// and instead emphasizes the map collection aspect more?
|
||
|
||
namespace UnityEngine.InputSystem
|
||
{
|
||
/// <summary>
|
||
/// An asset that contains action maps and control schemes.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// InputActionAssets can be created in code but are usually stored in JSON format on
|
||
/// disk with the ".inputactions" extension. Unity imports them with a custom
|
||
/// importer.
|
||
///
|
||
/// To create an InputActionAsset in code, use the <c>Singleton</c> API and populate the
|
||
/// asset with the methods found in <see cref="InputActionSetupExtensions"/>. Alternatively,
|
||
/// you can use <see cref="FromJson"/> to load an InputActionAsset directly from a string in JSON format.
|
||
///
|
||
/// <example>
|
||
/// <code>
|
||
/// // Create and configure an asset in code.
|
||
/// var asset1 = ScriptableObject.CreateInstance<InputActionAsset>();
|
||
/// var actionMap1 = asset1.AddActionMap("map1");
|
||
/// action1Map.AddAction("action1", binding: "<Keyboard>/space");
|
||
/// </code>
|
||
/// </example>
|
||
///
|
||
/// If you use the API to modify an InputActionAsset while in Play mode,
|
||
/// it does not survive the transition back to Edit Mode. Unity tracks and reloads modified assets
|
||
/// from disk when exiting Play mode. This is done so that you can realistically test the input
|
||
/// related functionality of your application i.e. control rebinding etc, without inadvertently changing
|
||
/// the input asset.
|
||
///
|
||
/// Each asset can contain arbitrary many action maps that you can enable and disable individually
|
||
/// (see <see cref="InputActionMap.Enable"/> and <see cref="InputActionMap.Disable"/>) or in bulk
|
||
/// (see <see cref="Enable"/> and <see cref="Disable"/>). The name of each action map must be unique.
|
||
/// The list of action maps can be queried from <see cref="actionMaps"/>.
|
||
///
|
||
/// InputActionAssets can only define <see cref="InputControlScheme"/>s. They can be added to
|
||
/// an asset with <see cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/>
|
||
/// and can be queried from <see cref="controlSchemes"/>.
|
||
///
|
||
/// Be aware that input action assets do not separate between static (configuration) data and dynamic
|
||
/// (instance) data. For audio, for example, <c>AudioClip</c> represents the static,
|
||
/// shared data portion of audio playback whereas <c>AudioSource"</c> represents the
|
||
/// dynamic, per-instance audio playback portion (referencing the clip through <c>AudioSource.clip</c>).
|
||
///
|
||
/// For input, such a split is less beneficial as the same input is generally not exercised
|
||
/// multiple times in parallel. Keeping both static and dynamic data together simplifies
|
||
/// using the system.
|
||
///
|
||
/// However, there are scenarios where you indeed want to take the same input action and
|
||
/// exercise it multiple times in parallel. A prominent example of such a use case is
|
||
/// local multiplayer where each player gets the same set of actions but is controlling
|
||
/// them with a different device (or devices) each. This is easily achieved by simply
|
||
/// using <c>UnityEngine.Object.Instantiate</c> to instantiate the input action
|
||
/// asset multiple times. <see cref="PlayerInput"/> will automatically do so in its
|
||
/// internals.
|
||
///
|
||
/// Note also that all action maps in an asset share binding state. This means that if
|
||
/// one map in an asset has to resolve its bindings, all maps in the asset have to.
|
||
/// </remarks>
|
||
public class InputActionAsset : ScriptableObject, IInputActionCollection2
|
||
{
|
||
/// <summary>
|
||
/// File extension (without the dot) for InputActionAssets in JSON format.
|
||
/// </summary>
|
||
/// <value>File extension for InputActionAsset source files.</value>
|
||
/// <remarks>
|
||
/// Files with this extension will automatically be imported by Unity as
|
||
/// InputActionAssets.
|
||
/// </remarks>
|
||
public const string Extension = "inputactions";
|
||
////REVIEW: actually pre-populate with some stuff?
|
||
internal const string kDefaultAssetLayoutJson = "{}";
|
||
|
||
/// <summary>
|
||
/// True if any action in the asset is currently enabled.
|
||
/// </summary>
|
||
/// <seealso cref="InputAction.enabled"/>
|
||
/// <seealso cref="InputActionMap.enabled"/>
|
||
/// <seealso cref="InputAction.Enable"/>
|
||
/// <seealso cref="InputActionMap.Enable"/>
|
||
/// <seealso cref="Enable"/>
|
||
public bool enabled
|
||
{
|
||
get
|
||
{
|
||
foreach (var actionMap in actionMaps)
|
||
if (actionMap.enabled)
|
||
return true;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// List of action maps defined in the asset.
|
||
/// </summary>
|
||
/// <value>Action maps contained in the asset.</value>
|
||
/// <seealso cref="InputActionSetupExtensions.AddActionMap(InputActionAsset,string)"/>
|
||
/// <seealso cref="InputActionSetupExtensions.RemoveActionMap(InputActionAsset,InputActionMap)"/>
|
||
/// <seealso cref="FindActionMap(string,bool)"/>
|
||
public ReadOnlyArray<InputActionMap> actionMaps => new ReadOnlyArray<InputActionMap>(m_ActionMaps);
|
||
|
||
/// <summary>
|
||
/// List of control schemes defined in the asset.
|
||
/// </summary>
|
||
/// <value>Control schemes defined for the asset.</value>
|
||
/// <seealso cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/>
|
||
/// <seealso cref="InputActionSetupExtensions.RemoveControlScheme"/>
|
||
public ReadOnlyArray<InputControlScheme> controlSchemes => new ReadOnlyArray<InputControlScheme>(m_ControlSchemes);
|
||
|
||
/// <summary>
|
||
/// Iterate over all bindings in the asset.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// This iterates over all action maps in <see cref="actionMaps"/> and, within each
|
||
/// map, over the set of <see cref="InputActionMap.bindings"/>.
|
||
/// </remarks>
|
||
/// <seealso cref="InputActionMap.bindings"/>
|
||
public IEnumerable<InputBinding> bindings
|
||
{
|
||
get
|
||
{
|
||
var numActionMaps = m_ActionMaps.LengthSafe();
|
||
if (numActionMaps == 0)
|
||
yield break;
|
||
|
||
for (var i = 0; i < numActionMaps; ++i)
|
||
{
|
||
var actionMap = m_ActionMaps[i];
|
||
var bindings = actionMap.m_Bindings;
|
||
var numBindings = bindings.LengthSafe();
|
||
|
||
for (var n = 0; n < numBindings; ++n)
|
||
yield return bindings[n];
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Binding mask to apply to all action maps and actions in the asset.
|
||
/// </summary>
|
||
/// <value>Optional mask that determines which bindings in the asset to enable.</value>
|
||
/// <remarks>
|
||
/// Binding masks can be applied at three different levels: for an entire asset through
|
||
/// this property, for a specific map through <see cref="InputActionMap.bindingMask"/>,
|
||
/// and for single actions through <see cref="InputAction.bindingMask"/>. By default,
|
||
/// none of the masks will be set (i.e. they will be <c>null</c>).
|
||
///
|
||
/// When an action is enabled, all the binding masks that apply to it are taken into
|
||
/// account. Specifically, this means that any given binding on the action will be
|
||
/// enabled only if it matches the mask applied to the asset, the mask applied
|
||
/// to the map that contains the action, and the mask applied to the action itself.
|
||
/// All the masks are individually optional.
|
||
///
|
||
/// Masks are matched against bindings using <see cref="InputBinding.Matches"/>.
|
||
///
|
||
/// Note that if you modify the masks applicable to an action while it is
|
||
/// enabled, the action's <see cref="InputAction.controls"/> will get updated immediately to
|
||
/// respect the mask. To avoid repeated binding resolution, it is most efficient
|
||
/// to apply binding masks before enabling actions.
|
||
///
|
||
/// Binding masks are non-destructive. All the bindings on the action are left
|
||
/// in place. Setting a mask will not affect the value of the <see cref="InputAction.bindings"/>
|
||
/// and <see cref="InputActionMap.bindings"/> properties.
|
||
/// </remarks>
|
||
/// <seealso cref="InputBinding.MaskByGroup"/>
|
||
/// <seealso cref="InputAction.bindingMask"/>
|
||
/// <seealso cref="InputActionMap.bindingMask"/>
|
||
public InputBinding? bindingMask
|
||
{
|
||
get => m_BindingMask;
|
||
set
|
||
{
|
||
if (m_BindingMask == value)
|
||
return;
|
||
|
||
m_BindingMask = value;
|
||
|
||
ReResolveIfNecessary(fullResolve: true);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Set of devices that bindings in the asset can bind to.
|
||
/// </summary>
|
||
/// <value>Optional set of devices to use by bindings in the asset.</value>
|
||
/// <remarks>
|
||
/// By default (with this property being <c>null</c>), bindings will bind to any of the
|
||
/// controls available through <see cref="InputSystem.devices"/>, i.e. controls from all
|
||
/// devices in the system will be used.
|
||
///
|
||
/// By setting this property, binding resolution can instead be restricted to just specific
|
||
/// devices. This restriction can either be applied to an entire asset using this property
|
||
/// or to specific action maps by using <see cref="InputActionMap.devices"/>. Note that if
|
||
/// both this property and <see cref="InputActionMap.devices"/> is set for a specific action
|
||
/// map, the list of devices on the action map will take precedence and the list on the
|
||
/// asset will be ignored for bindings in that action map.
|
||
///
|
||
/// <example>
|
||
/// <code>
|
||
/// // Create an asset with a single action map and a single action with a
|
||
/// // gamepad binding.
|
||
/// var asset = ScriptableObject.CreateInstance<InputActionAsset>();
|
||
/// var actionMap = new InputActionMap();
|
||
/// var fireAction = actionMap.AddAction("Fire", binding: "<Gamepad>/buttonSouth");
|
||
/// asset.AddActionMap(actionMap);
|
||
///
|
||
/// // Let's assume we have two gamepads connected. If we enable the
|
||
/// // action map now, the 'Fire' action will bind to both.
|
||
/// actionMap.Enable();
|
||
///
|
||
/// // This will print two controls.
|
||
/// Debug.Log(string.Join("\n", fireAction.controls));
|
||
///
|
||
/// // To restrict the setup to just the first gamepad, we can assign
|
||
/// // to the 'devices' property (in this case, we could do so on either
|
||
/// // the action map or on the asset; we choose the latter here).
|
||
/// asset.devices = new InputDevice[] { Gamepad.all[0] };
|
||
///
|
||
/// // Now this will print only one control.
|
||
/// Debug.Log(string.Join("\n", fireAction.controls));
|
||
/// </code>
|
||
/// </example>
|
||
/// </remarks>
|
||
/// <seealso cref="InputActionMap.devices"/>
|
||
public ReadOnlyArray<InputDevice>? devices
|
||
{
|
||
get => m_Devices.Get();
|
||
set
|
||
{
|
||
if (m_Devices.Set(value))
|
||
ReResolveIfNecessary(fullResolve: false);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Look up an action by name or ID.
|
||
/// </summary>
|
||
/// <param name="actionNameOrId">Name of the action as either a "map/action" combination (e.g. "gameplay/fire") or
|
||
/// a simple name. In the former case, the name is split at the '/' slash and the first part is used to find
|
||
/// a map with that name and the second part is used to find an action with that name inside the map. In the
|
||
/// latter case, all maps are searched in order and the first action that has the given name in any of the maps
|
||
/// is returned. Note that name comparisons are case-insensitive.
|
||
///
|
||
/// Alternatively, the given string can be a GUID as given by <see cref="InputAction.id"/>.</param>
|
||
/// <returns>The action with the corresponding name or null if no matching action could be found.</returns>
|
||
/// <remarks>
|
||
/// This method is equivalent to <see cref="FindAction(string,bool)"/> except that it throws
|
||
/// <see cref="KeyNotFoundException"/> if no action with the given name or ID
|
||
/// could be found.
|
||
/// </remarks>
|
||
/// <exception cref="KeyNotFoundException">No action was found matching <paramref name="actionNameOrId"/>.</exception>
|
||
/// <exception cref="ArgumentNullException"><paramref name="actionNameOrId"/> is <c>null</c> or empty.</exception>
|
||
/// <seealso cref="FindAction(string,bool)"/>
|
||
public InputAction this[string actionNameOrId]
|
||
{
|
||
get
|
||
{
|
||
var action = FindAction(actionNameOrId);
|
||
if (action == null)
|
||
throw new KeyNotFoundException($"Cannot find action '{actionNameOrId}' in '{this}'");
|
||
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.
|
||
/// </summary>
|
||
/// <returns>A string in JSON format that represents the static/configuration data present
|
||
/// in the asset.</returns>
|
||
/// <remarks>
|
||
/// This will not save dynamic execution state such as callbacks installed on
|
||
/// <see cref="InputAction">actions</see> or enabled/disabled states of individual
|
||
/// maps and actions.
|
||
///
|
||
/// Use <see cref="LoadFromJson"/> to deserialize the JSON data back into an InputActionAsset.
|
||
///
|
||
/// Be aware that the format used by this method is <em>different</em> than what you
|
||
/// get if you call <c>JsonUtility.ToJson</c> on an InputActionAsset instance. In other
|
||
/// words, the JSON format is not identical to the Unity serialized object representation
|
||
/// of the asset.
|
||
/// </remarks>
|
||
/// <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),
|
||
}, true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Replace the contents of the asset with the data in the given JSON string.
|
||
/// </summary>
|
||
/// <param name="json">JSON contents of an <c>.inputactions</c> asset.</param>
|
||
/// <remarks>
|
||
/// <c>.inputactions</c> assets are stored in JSON format. This method allows reading
|
||
/// the JSON source text of such an asset into an existing <c>InputActionMap</c> instance.
|
||
///
|
||
/// <example>
|
||
/// <code>
|
||
/// var asset = ScriptableObject.CreateInstance<InputActionAsset>();
|
||
/// asset.LoadFromJson(@"
|
||
/// {
|
||
/// ""maps"" : [
|
||
/// {
|
||
/// ""name"" : ""gameplay"",
|
||
/// ""actions"" : [
|
||
/// { ""name"" : ""fire"", ""type"" : ""button"" },
|
||
/// { ""name"" : ""look"", ""type"" : ""value"" },
|
||
/// { ""name"" : ""move"", ""type"" : ""value"" }
|
||
/// ],
|
||
/// ""bindings"" : [
|
||
/// { ""path"" : ""<Gamepad>/buttonSouth"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" },
|
||
/// { ""path"" : ""<Gamepad>/leftTrigger"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" },
|
||
/// { ""path"" : ""<Gamepad>/leftStick"", ""action"" : ""move"", ""groups"" : ""Gamepad"" },
|
||
/// { ""path"" : ""<Gamepad>/rightStick"", ""action"" : ""look"", ""groups"" : ""Gamepad"" },
|
||
/// { ""path"" : ""dpad"", ""action"" : ""move"", ""groups"" : ""Gamepad"", ""isComposite"" : true },
|
||
/// { ""path"" : ""<Keyboard>/a"", ""name"" : ""left"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
|
||
/// { ""path"" : ""<Keyboard>/d"", ""name"" : ""right"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
|
||
/// { ""path"" : ""<Keyboard>/w"", ""name"" : ""up"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
|
||
/// { ""path"" : ""<Keyboard>/s"", ""name"" : ""down"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
|
||
/// { ""path"" : ""<Mouse>/delta"", ""action"" : ""look"", ""groups"" : ""Keyboard&Mouse"" },
|
||
/// { ""path"" : ""<Mouse>/leftButton"", ""action"" : ""fire"", ""groups"" : ""Keyboard&Mouse"" }
|
||
/// ]
|
||
/// },
|
||
/// {
|
||
/// ""name"" : ""ui"",
|
||
/// ""actions"" : [
|
||
/// { ""name"" : ""navigate"" }
|
||
/// ],
|
||
/// ""bindings"" : [
|
||
/// { ""path"" : ""<Gamepad>/dpad"", ""action"" : ""navigate"", ""groups"" : ""Gamepad"" }
|
||
/// ]
|
||
/// }
|
||
/// ],
|
||
/// ""controlSchemes"" : [
|
||
/// {
|
||
/// ""name"" : ""Gamepad"",
|
||
/// ""bindingGroup"" : ""Gamepad"",
|
||
/// ""devices"" : [
|
||
/// { ""devicePath"" : ""<Gamepad>"" }
|
||
/// ]
|
||
/// },
|
||
/// {
|
||
/// ""name"" : ""Keyboard&Mouse"",
|
||
/// ""bindingGroup"" : ""Keyboard&Mouse"",
|
||
/// ""devices"" : [
|
||
/// { ""devicePath"" : ""<Keyboard>"" },
|
||
/// { ""devicePath"" : ""<Mouse>"" }
|
||
/// ]
|
||
/// }
|
||
/// ]
|
||
/// }");
|
||
/// </code>
|
||
/// </example>
|
||
/// </remarks>
|
||
/// <exception cref="ArgumentNullException"><paramref name="json"/> is <c>null</c> or empty.</exception>
|
||
/// <seealso cref="FromJson"/>
|
||
/// <seealso cref="ToJson"/>
|
||
public void LoadFromJson(string json)
|
||
{
|
||
if (string.IsNullOrEmpty(json))
|
||
throw new ArgumentNullException(nameof(json));
|
||
|
||
var parsedJson = JsonUtility.FromJson<ReadFileJson>(json);
|
||
MigrateJson(ref parsedJson);
|
||
parsedJson.ToAsset(this);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Replace the contents of the asset with the data in the given JSON string.
|
||
/// </summary>
|
||
/// <param name="json">JSON contents of an <c>.inputactions</c> asset.</param>
|
||
/// <returns>The InputActionAsset instance created from the given JSON string.</returns>
|
||
/// <remarks>
|
||
/// <c>.inputactions</c> assets are stored in JSON format. This method allows turning
|
||
/// the JSON source text of such an asset into a new <c>InputActionMap</c> instance.
|
||
///
|
||
/// Be aware that the format used by this method is <em>different</em> than what you
|
||
/// get if you call <c>JsonUtility.ToJson</c> on an InputActionAsset instance. In other
|
||
/// words, the JSON format is not identical to the Unity serialized object representation
|
||
/// of the asset.
|
||
///
|
||
/// <example>
|
||
/// <code>
|
||
/// var asset = InputActionAsset.FromJson(@"
|
||
/// {
|
||
/// ""maps"" : [
|
||
/// {
|
||
/// ""name"" : ""gameplay"",
|
||
/// ""actions"" : [
|
||
/// { ""name"" : ""fire"", ""type"" : ""button"" },
|
||
/// { ""name"" : ""look"", ""type"" : ""value"" },
|
||
/// { ""name"" : ""move"", ""type"" : ""value"" }
|
||
/// ],
|
||
/// ""bindings"" : [
|
||
/// { ""path"" : ""<Gamepad>/buttonSouth"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" },
|
||
/// { ""path"" : ""<Gamepad>/leftTrigger"", ""action"" : ""fire"", ""groups"" : ""Gamepad"" },
|
||
/// { ""path"" : ""<Gamepad>/leftStick"", ""action"" : ""move"", ""groups"" : ""Gamepad"" },
|
||
/// { ""path"" : ""<Gamepad>/rightStick"", ""action"" : ""look"", ""groups"" : ""Gamepad"" },
|
||
/// { ""path"" : ""dpad"", ""action"" : ""move"", ""groups"" : ""Gamepad"", ""isComposite"" : true },
|
||
/// { ""path"" : ""<Keyboard>/a"", ""name"" : ""left"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
|
||
/// { ""path"" : ""<Keyboard>/d"", ""name"" : ""right"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
|
||
/// { ""path"" : ""<Keyboard>/w"", ""name"" : ""up"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
|
||
/// { ""path"" : ""<Keyboard>/s"", ""name"" : ""down"", ""action"" : ""move"", ""groups"" : ""Keyboard&Mouse"", ""isPartOfComposite"" : true },
|
||
/// { ""path"" : ""<Mouse>/delta"", ""action"" : ""look"", ""groups"" : ""Keyboard&Mouse"" },
|
||
/// { ""path"" : ""<Mouse>/leftButton"", ""action"" : ""fire"", ""groups"" : ""Keyboard&Mouse"" }
|
||
/// ]
|
||
/// },
|
||
/// {
|
||
/// ""name"" : ""ui"",
|
||
/// ""actions"" : [
|
||
/// { ""name"" : ""navigate"" }
|
||
/// ],
|
||
/// ""bindings"" : [
|
||
/// { ""path"" : ""<Gamepad>/dpad"", ""action"" : ""navigate"", ""groups"" : ""Gamepad"" }
|
||
/// ]
|
||
/// }
|
||
/// ],
|
||
/// ""controlSchemes"" : [
|
||
/// {
|
||
/// ""name"" : ""Gamepad"",
|
||
/// ""bindingGroup"" : ""Gamepad"",
|
||
/// ""devices"" : [
|
||
/// { ""devicePath"" : ""<Gamepad>"" }
|
||
/// ]
|
||
/// },
|
||
/// {
|
||
/// ""name"" : ""Keyboard&Mouse"",
|
||
/// ""bindingGroup"" : ""Keyboard&Mouse"",
|
||
/// ""devices"" : [
|
||
/// { ""devicePath"" : ""<Keyboard>"" },
|
||
/// { ""devicePath"" : ""<Mouse>"" }
|
||
/// ]
|
||
/// }
|
||
/// ]
|
||
/// }");
|
||
/// </code>
|
||
/// </example>
|
||
/// </remarks>
|
||
/// <exception cref="ArgumentNullException"><paramref name="json"/> is <c>null</c> or empty.</exception>
|
||
/// <seealso cref="LoadFromJson"/>
|
||
/// <seealso cref="ToJson"/>
|
||
public static InputActionAsset FromJson(string json)
|
||
{
|
||
if (string.IsNullOrEmpty(json))
|
||
throw new ArgumentNullException(nameof(json));
|
||
|
||
var asset = CreateInstance<InputActionAsset>();
|
||
asset.LoadFromJson(json);
|
||
return asset;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Find an <see cref="InputAction"/> by its name in one of the <see cref="InputActionMap"/>s
|
||
/// in the asset.
|
||
/// </summary>
|
||
/// <param name="actionNameOrId">Name of the action as either a "map/action" combination (e.g. "gameplay/fire") or
|
||
/// a simple name (e.g. "fire"). In the former case, the name is split at the '/' slash and the first part is used to find
|
||
/// a map with that name and the second part is used to find an action with that name inside the map. In the
|
||
/// latter case, all maps are searched in order and the first action that has the given name in any of the maps
|
||
/// is returned. Note that name comparisons are case-insensitive.
|
||
///
|
||
/// Alternatively, the given string can be a GUID as given by <see cref="InputAction.id"/>.</param>
|
||
/// <param name="throwIfNotFound">If <c>true</c>, instead of returning <c>null</c> when the action
|
||
/// cannot be found, throw <c>ArgumentException</c>.</param>
|
||
/// <returns>The action with the corresponding name or <c>null</c> if no matching action could be found.</returns>
|
||
/// <remarks>
|
||
/// Note that no lookup structures are used internally to speed the operation up. Instead, the search is done
|
||
/// linearly. For repeated access of an action, it is thus generally best to look up actions once ahead of
|
||
/// time and cache the result.
|
||
///
|
||
/// If multiple actions have the same name and <paramref name="actionNameOrId"/> is not an ID and not an
|
||
/// action name qualified by a map name (that is, in the form of <c>"mapName/actionName"</c>), the action that
|
||
/// is returned will be from the first map in <see cref="actionMaps"/> that has an action with the given name.
|
||
/// An exception is if, of the multiple actions with the same name, some are enabled and some are disabled. In
|
||
/// this case, the first action that is enabled is returned.
|
||
///
|
||
/// If an action name contains a slash "/", e.g. "yaw/pitch" and there is also a map called "yaw" which
|
||
/// contains an action "pitch", the action "pitch" within the map "yaw" will be returned instead of the
|
||
/// action named "yaw/pitch".
|
||
///
|
||
/// <example>
|
||
/// <code>
|
||
/// var asset = ScriptableObject.CreateInstance<InputActionAsset>();
|
||
///
|
||
/// var map1 = new InputActionMap("map1");
|
||
/// var map2 = new InputActionMap("map2");
|
||
///
|
||
/// asset.AddActionMap(map1);
|
||
/// asset.AddActionMap(map2);
|
||
///
|
||
/// var action1 = map1.AddAction("action1");
|
||
/// var action2 = map1.AddAction("action2");
|
||
/// var action3 = map2.AddAction("action3");
|
||
///
|
||
/// // Search all maps in the asset for any action that has the given name.
|
||
/// asset.FindAction("action1") // Returns action1.
|
||
/// asset.FindAction("action2") // Returns action2
|
||
/// asset.FindAction("action3") // Returns action3.
|
||
///
|
||
/// // Search for a specific action in a specific map.
|
||
/// asset.FindAction("map1/action1") // Returns action1.
|
||
/// asset.FindAction("map2/action2") // Returns action2.
|
||
/// asset.FindAction("map3/action3") // Returns action3.
|
||
///
|
||
/// // Search by unique action ID.
|
||
/// asset.FindAction(action1.id.ToString()) // Returns action1.
|
||
/// asset.FindAction(action2.id.ToString()) // Returns action2.
|
||
/// asset.FindAction(action3.id.ToString()) // Returns action3.
|
||
/// </code>
|
||
/// </example>
|
||
/// </remarks>
|
||
/// <exception cref="ArgumentNullException"><paramref name="actionNameOrId"/> is <c>null</c>.</exception>
|
||
/// <exception cref="ArgumentException">Thrown if <paramref name="throwIfNotFound"/> is true and the
|
||
/// action could not be found. -Or- If <paramref name="actionNameOrId"/> contains a slash but is missing
|
||
/// either the action or the map name.</exception>
|
||
public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false)
|
||
{
|
||
if (actionNameOrId == null)
|
||
throw new ArgumentNullException(nameof(actionNameOrId));
|
||
|
||
if (m_ActionMaps != null)
|
||
{
|
||
// Check if we have a "map/action" path. If we do we either has a "map/action" path or a simple
|
||
// action name containing a slash. We first attempt matching it to a "map/action" and only if that
|
||
// fails do we attempt to search for a "some/action" name.
|
||
var indexOfSlash = actionNameOrId.IndexOf('/');
|
||
if (indexOfSlash >= 0)
|
||
{
|
||
// Have a path. First search for the map, then for the action.
|
||
var mapName = new Substring(actionNameOrId, 0, indexOfSlash);
|
||
var actionName = new Substring(actionNameOrId, indexOfSlash + 1);
|
||
|
||
if (mapName.isEmpty || actionName.isEmpty)
|
||
throw new ArgumentException("Malformed action path: " + actionNameOrId, nameof(actionNameOrId));
|
||
|
||
for (var i = 0; i < m_ActionMaps.Length; ++i)
|
||
{
|
||
var map = m_ActionMaps[i];
|
||
if (Substring.Compare(map.name, mapName, StringComparison.InvariantCultureIgnoreCase) != 0)
|
||
continue;
|
||
|
||
var actions = map.m_Actions;
|
||
if (actions != null)
|
||
{
|
||
for (var n = 0; n < actions.Length; ++n)
|
||
{
|
||
var action = actions[n];
|
||
if (Substring.Compare(action.name, actionName,
|
||
StringComparison.InvariantCultureIgnoreCase) == 0)
|
||
return action;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Check if there is an action with the given name regardless of containing map.
|
||
// If multiple actions exist with the same identifier, the first enabled one is returned.
|
||
// If no enabled action exist, the first is returned.
|
||
InputAction firstActionFound = null;
|
||
for (var i = 0; i < m_ActionMaps.Length; ++i)
|
||
{
|
||
var action = m_ActionMaps[i].FindAction(actionNameOrId);
|
||
if (action != null)
|
||
{
|
||
if (action.enabled || action.m_Id == actionNameOrId) // Match by ID is always exact.
|
||
return action;
|
||
if (firstActionFound == null)
|
||
firstActionFound = action;
|
||
}
|
||
}
|
||
if (firstActionFound != null)
|
||
return firstActionFound;
|
||
}
|
||
|
||
if (throwIfNotFound)
|
||
throw new ArgumentException($"No action '{actionNameOrId}' in '{this}'");
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public int FindBinding(InputBinding mask, out InputAction action)
|
||
{
|
||
var numMaps = m_ActionMaps.LengthSafe();
|
||
|
||
for (var i = 0; i < numMaps; ++i)
|
||
{
|
||
var actionMap = m_ActionMaps[i];
|
||
|
||
var bindingIndex = actionMap.FindBinding(mask, out action);
|
||
if (bindingIndex >= 0)
|
||
return bindingIndex;
|
||
}
|
||
|
||
action = null;
|
||
return -1;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Find an <see cref="InputActionMap"/> in the asset by its name or ID.
|
||
/// </summary>
|
||
/// <param name="nameOrId">Name or ID (see <see cref="InputActionMap.id"/>) of the action map
|
||
/// to look for. Matching is case-insensitive.</param>
|
||
/// <param name="throwIfNotFound">If true, instead of returning <c>null</c>, throw <c>ArgumentException</c>.</param>
|
||
/// <returns>The <see cref="InputActionMap"/> with a name or ID matching <paramref name="nameOrId"/> or
|
||
/// <c>null</c> if no matching map could be found.</returns>
|
||
/// <exception cref="ArgumentNullException"><paramref name="nameOrId"/> is <c>null</c>.</exception>
|
||
/// <exception cref="ArgumentException">If <paramref name="throwIfNotFound"/> is <c>true</c>, thrown if
|
||
/// the action map cannot be found.</exception>
|
||
/// <seealso cref="actionMaps"/>
|
||
/// <seealso cref="FindActionMap(System.Guid)"/>
|
||
public InputActionMap FindActionMap(string nameOrId, bool throwIfNotFound = false)
|
||
{
|
||
if (nameOrId == null)
|
||
throw new ArgumentNullException(nameof(nameOrId));
|
||
|
||
if (m_ActionMaps == null)
|
||
return null;
|
||
|
||
// If the name contains a hyphen, it may be a GUID.
|
||
if (nameOrId.Contains('-') && Guid.TryParse(nameOrId, out var id))
|
||
{
|
||
for (var i = 0; i < m_ActionMaps.Length; ++i)
|
||
{
|
||
var map = m_ActionMaps[i];
|
||
if (map.idDontGenerate == id)
|
||
return map;
|
||
}
|
||
}
|
||
|
||
// Default lookup is by name (case-insensitive).
|
||
for (var i = 0; i < m_ActionMaps.Length; ++i)
|
||
{
|
||
var map = m_ActionMaps[i];
|
||
if (string.Compare(nameOrId, map.name, StringComparison.InvariantCultureIgnoreCase) == 0)
|
||
return map;
|
||
}
|
||
|
||
if (throwIfNotFound)
|
||
throw new ArgumentException($"Cannot find action map '{nameOrId}' in '{this}'");
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Find an <see cref="InputActionMap"/> in the asset by its ID.
|
||
/// </summary>
|
||
/// <param name="id">ID (see <see cref="InputActionMap.id"/>) of the action map
|
||
/// to look for.</param>
|
||
/// <returns>The <see cref="InputActionMap"/> with ID matching <paramref name="id"/> or
|
||
/// <c>null</c> if no map in the asset has the given ID.</returns>
|
||
/// <seealso cref="actionMaps"/>
|
||
/// <seealso cref="FindActionMap"/>
|
||
public InputActionMap FindActionMap(Guid id)
|
||
{
|
||
if (m_ActionMaps == null)
|
||
return null;
|
||
|
||
for (var i = 0; i < m_ActionMaps.Length; ++i)
|
||
{
|
||
var map = m_ActionMaps[i];
|
||
if (map.idDontGenerate == id)
|
||
return map;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Find an action by its ID (see <see cref="InputAction.id"/>).
|
||
/// </summary>
|
||
/// <param name="guid">ID of the action to look for.</param>
|
||
/// <returns>The action in the asset with the given ID or null if no action
|
||
/// in the asset has the given ID.</returns>
|
||
public InputAction FindAction(Guid guid)
|
||
{
|
||
if (m_ActionMaps == null)
|
||
return null;
|
||
|
||
for (var i = 0; i < m_ActionMaps.Length; ++i)
|
||
{
|
||
var map = m_ActionMaps[i];
|
||
var action = map.FindAction(guid);
|
||
if (action != null)
|
||
return action;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Find the control scheme with the given name and return its index
|
||
/// in <see cref="controlSchemes"/>.
|
||
/// </summary>
|
||
/// <param name="name">Name of the control scheme. Matching is case-insensitive.</param>
|
||
/// <returns>The index of the given control scheme or -1 if no control scheme
|
||
/// with the given name could be found.</returns>
|
||
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>
|
||
/// or empty.</exception>
|
||
public int FindControlSchemeIndex(string name)
|
||
{
|
||
if (string.IsNullOrEmpty(name))
|
||
throw new ArgumentNullException(nameof(name));
|
||
|
||
if (m_ControlSchemes == null)
|
||
return -1;
|
||
|
||
for (var i = 0; i < m_ControlSchemes.Length; ++i)
|
||
if (string.Compare(name, m_ControlSchemes[i].name, StringComparison.InvariantCultureIgnoreCase) == 0)
|
||
return i;
|
||
|
||
return -1;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Find the control scheme with the given name and return it.
|
||
/// </summary>
|
||
/// <param name="name">Name of the control scheme. Matching is case-insensitive.</param>
|
||
/// <returns>The control scheme with the given name or null if no scheme
|
||
/// with the given name could be found in the asset.</returns>
|
||
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>
|
||
/// or empty.</exception>
|
||
public InputControlScheme? FindControlScheme(string name)
|
||
{
|
||
if (string.IsNullOrEmpty(name))
|
||
throw new ArgumentNullException(nameof(name));
|
||
|
||
var index = FindControlSchemeIndex(name);
|
||
if (index == -1)
|
||
return null;
|
||
|
||
return m_ControlSchemes[index];
|
||
}
|
||
|
||
/// <summary>
|
||
/// Return true if the asset contains bindings (in any of its action maps) that are usable
|
||
/// with the given <paramref name="device"/>.
|
||
/// </summary>
|
||
/// <param name="device">An arbitrary input device.</param>
|
||
/// <returns></returns>
|
||
/// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception>
|
||
/// <remarks>
|
||
/// <example>
|
||
/// <code>
|
||
/// // Find out if the actions of the given PlayerInput can be used with
|
||
/// // a gamepad.
|
||
/// if (playerInput.actions.IsUsableWithDevice(Gamepad.all[0]))
|
||
/// /* ... */;
|
||
/// </code>
|
||
/// </example>
|
||
/// </remarks>
|
||
/// <seealso cref="InputActionMap.IsUsableWithDevice"/>
|
||
/// <seealso cref="InputControlScheme.SupportsDevice"/>
|
||
public bool IsUsableWithDevice(InputDevice device)
|
||
{
|
||
if (device == null)
|
||
throw new ArgumentNullException(nameof(device));
|
||
|
||
// If we have control schemes, we let those dictate our search.
|
||
var numControlSchemes = m_ControlSchemes.LengthSafe();
|
||
if (numControlSchemes > 0)
|
||
{
|
||
for (var i = 0; i < numControlSchemes; ++i)
|
||
{
|
||
if (m_ControlSchemes[i].SupportsDevice(device))
|
||
return true;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Otherwise, we'll go search bindings. Slow.
|
||
var actionMapCount = m_ActionMaps.LengthSafe();
|
||
for (var i = 0; i < actionMapCount; ++i)
|
||
if (m_ActionMaps[i].IsUsableWithDevice(device))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Enable all action maps in the asset.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// This method is equivalent to calling <see cref="InputActionMap.Enable"/> on
|
||
/// all maps in <see cref="actionMaps"/>.
|
||
/// </remarks>
|
||
public void Enable()
|
||
{
|
||
foreach (var map in actionMaps)
|
||
map.Enable();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Disable all action maps in the asset.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// This method is equivalent to calling <see cref="InputActionMap.Disable"/> on
|
||
/// all maps in <see cref="actionMaps"/>.
|
||
/// </remarks>
|
||
public void Disable()
|
||
{
|
||
foreach (var map in actionMaps)
|
||
map.Disable();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Return <c>true</c> if the given action is part of the asset.
|
||
/// </summary>
|
||
/// <param name="action">An action. Can be null.</param>
|
||
/// <returns>True if the given action is part of the asset, false otherwise.</returns>
|
||
public bool Contains(InputAction action)
|
||
{
|
||
var map = action?.actionMap;
|
||
if (map == null)
|
||
return false;
|
||
|
||
return map.asset == this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Enumerate all actions in the asset.
|
||
/// </summary>
|
||
/// <returns>An enumerator going over the actions in the asset.</returns>
|
||
/// <remarks>
|
||
/// Actions will be enumerated one action map in <see cref="actionMaps"/>
|
||
/// after the other. The actions from each map will be yielded in turn.
|
||
///
|
||
/// This method will allocate GC heap memory.
|
||
/// </remarks>
|
||
public IEnumerator<InputAction> GetEnumerator()
|
||
{
|
||
if (m_ActionMaps == null)
|
||
yield break;
|
||
|
||
for (var i = 0; i < m_ActionMaps.Length; ++i)
|
||
{
|
||
var actions = m_ActionMaps[i].actions;
|
||
var actionCount = actions.Count;
|
||
|
||
for (var n = 0; n < actionCount; ++n)
|
||
yield return actions[n];
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Enumerate all actions in the asset.
|
||
/// </summary>
|
||
/// <returns>An enumerator going over the actions in the asset.</returns>
|
||
/// <seealso cref="GetEnumerator"/>
|
||
IEnumerator IEnumerable.GetEnumerator()
|
||
{
|
||
return GetEnumerator();
|
||
}
|
||
|
||
internal void MarkAsDirty()
|
||
{
|
||
#if UNITY_EDITOR
|
||
InputSystem.TrackDirtyInputActionAsset(this);
|
||
#endif
|
||
}
|
||
|
||
internal bool IsEmpty()
|
||
{
|
||
return actionMaps.Count == 0 && controlSchemes.Count == 0;
|
||
}
|
||
|
||
internal void OnWantToChangeSetup()
|
||
{
|
||
if (m_ActionMaps.LengthSafe() > 0)
|
||
m_ActionMaps[0].OnWantToChangeSetup();
|
||
}
|
||
|
||
internal void OnSetupChanged()
|
||
{
|
||
MarkAsDirty();
|
||
|
||
if (m_ActionMaps.LengthSafe() > 0)
|
||
m_ActionMaps[0].OnSetupChanged();
|
||
else
|
||
m_SharedStateForAllMaps = null;
|
||
}
|
||
|
||
private void ReResolveIfNecessary(bool fullResolve)
|
||
{
|
||
if (m_SharedStateForAllMaps == null)
|
||
return;
|
||
|
||
Debug.Assert(m_ActionMaps != null && m_ActionMaps.Length > 0);
|
||
// State is share between all action maps in the asset. Resolving bindings for the
|
||
// first map will resolve them for all maps.
|
||
m_ActionMaps[0].LazyResolveBindings(fullResolve);
|
||
}
|
||
|
||
internal void ResolveBindingsIfNecessary()
|
||
{
|
||
if (m_ActionMaps.LengthSafe() > 0)
|
||
foreach (var map in m_ActionMaps)
|
||
if (map.ResolveBindingsIfNecessary())
|
||
break;
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
Disable();
|
||
if (m_SharedStateForAllMaps != null)
|
||
{
|
||
m_SharedStateForAllMaps.Dispose(); // Will clean up InputActionMap state.
|
||
m_SharedStateForAllMaps = null;
|
||
}
|
||
}
|
||
|
||
////TODO: ApplyBindingOverrides, RemoveBindingOverrides, RemoveAllBindingOverrides
|
||
|
||
[SerializeField] internal InputActionMap[] m_ActionMaps;
|
||
[SerializeField] internal InputControlScheme[] m_ControlSchemes;
|
||
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
|
||
[SerializeField] internal bool m_IsProjectWide;
|
||
#endif
|
||
|
||
////TODO: make this persistent across domain reloads
|
||
/// <summary>
|
||
/// Shared state for all action maps in the asset.
|
||
/// </summary>
|
||
[NonSerialized] internal InputActionState m_SharedStateForAllMaps;
|
||
[NonSerialized] internal InputBinding? m_BindingMask;
|
||
[NonSerialized] internal int m_ParameterOverridesCount;
|
||
[NonSerialized] internal InputActionRebindingExtensions.ParameterOverride[] m_ParameterOverrides;
|
||
|
||
[NonSerialized] internal InputActionMap.DeviceArray m_Devices;
|
||
|
||
[Serializable]
|
||
internal struct WriteFileJson
|
||
{
|
||
public int version;
|
||
public string name;
|
||
public InputActionMap.WriteMapJson[] maps;
|
||
public InputControlScheme.SchemeJson[] controlSchemes;
|
||
}
|
||
|
||
[Serializable]
|
||
internal struct WriteFileJsonNoName
|
||
{
|
||
public InputActionMap.WriteMapJson[] maps;
|
||
public InputControlScheme.SchemeJson[] controlSchemes;
|
||
}
|
||
|
||
[Serializable]
|
||
internal struct ReadFileJson
|
||
{
|
||
public int version;
|
||
public string name;
|
||
public InputActionMap.ReadMapJson[] maps;
|
||
public InputControlScheme.SchemeJson[] controlSchemes;
|
||
|
||
public void ToAsset(InputActionAsset asset)
|
||
{
|
||
asset.name = name;
|
||
asset.m_ActionMaps = new InputActionMap.ReadFileJson {maps = maps}.ToMaps();
|
||
asset.m_ControlSchemes = InputControlScheme.SchemeJson.ToSchemes(controlSchemes);
|
||
|
||
// Link maps to their asset.
|
||
if (asset.m_ActionMaps != null)
|
||
foreach (var map in asset.m_ActionMaps)
|
||
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;
|
||
}
|
||
}
|
||
}
|