#if ENABLE_INPUT_SYSTEM
using System.Collections.Generic;
using System.IO;
using System.Text;
using Invector.vCharacterController;
using UnityEditor;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.UI;
using UnityEngine.EventSystems;
namespace Invector.vCharacterController.InputSystemEditor
{
internal static class vInputSystemMigrationUtility
{
private const string InputActionsPath = "Assets/Invector-3rdPersonController/Basic Locomotion/InputSystem/InvectorPlayerInputs.inputactions";
private const string BasicControllerPath = "Assets/Invector-3rdPersonController/Basic Locomotion/Prefabs/vBasicController.prefab";
private const string MeleeInventoryControllerPath = "Assets/Invector-3rdPersonController/Melee Combat/Prefabs/Player/vMeleeController_Inventory.prefab";
private const string MeleeNoInventoryControllerPath = "Assets/Invector-3rdPersonController/Melee Combat/Prefabs/Player/vMeleeController_NoInventory.prefab";
private const string ShooterMeleeInventoryControllerPath = "Assets/Invector-3rdPersonController/Shooter/Prefabs/Player/vShooterMelee_Inventory.prefab";
private const string ShooterMeleeNoInventoryControllerPath = "Assets/Invector-3rdPersonController/Shooter/Prefabs/Player/vShooterMelee_NoInventory.prefab";
private const string ShooterOnlyInventoryControllerPath = "Assets/Invector-3rdPersonController/Shooter/Prefabs/Player/vShooterOnly_Inventory.prefab";
private const string GameStyleSelectorMeleePath = "Assets/Invector-3rdPersonController/Melee Combat/Prefabs/UI/GameStyleSelector_Melee.prefab";
private const string GameStyleSelectorShooterPath = "Assets/Invector-3rdPersonController/Shooter/Prefabs/UI/GameStyleSelector_Shooter.prefab";
private const string InventoryMeleeOnlyPath = "Assets/Invector-3rdPersonController/ItemManager/Prefabs/Inventory_MeleeOnly.prefab";
private const string InventoryShooterMeleePath = "Assets/Invector-3rdPersonController/ItemManager/Prefabs/Inventory_ShooterMelee.prefab";
private const string InventoryShooterOnlyPath = "Assets/Invector-3rdPersonController/ItemManager/Prefabs/Inventory_ShooterOnly.prefab";
private static readonly string[] AddonControllerPrefabPaths =
{
"Assets/Invector-3rdPersonController/Add-ons/Controller_2.5D Platform/Basic/Prefabs/v2.5DController.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_2.5D Platform/Basic/Prefabs/PlatformLevelExample.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_2.5D Platform/Melee (Require MeleeCombat Template)/Prefabs/v2.5D_MeleeController.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_2.5D Platform/Shooter (Require Shooter Template)/Prefabs/Shooter2.5D.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_2.5D Platform/Shooter (Require Shooter Template)/Prefabs/MobileShooter2.5D.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_Point&Click/Basic/Prefabs/Basic_Point&ClickController.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_Point&Click/Melee (Require MeleeCombat Template)/Prefabs/Melee_Point&ClickController.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_Point&Click/Melee (Require MeleeCombat Template)/Prefabs/Melee_Point&ClickAgent.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_TopDown/Basic/Prefabs/vTopDownController.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_TopDown/Melee (Required Melee Template)/Prefabs/vTopDownMeleeController.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_TopDown/Shooter (Require Shooter Template)/Prefabs/ShooterMeleeTopDown.prefab"
};
private static readonly string[] MobileControllerPrefabPaths =
{
"Assets/Invector-3rdPersonController/Add-ons/Controller_Mobile/Basic/Prefabs/vBasicMobileController.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_Mobile/Melee (Require MeleeCombat Template)/Prefabs/MeleeControllerMobile_Inventory.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_Mobile/Melee (Require MeleeCombat Template)/Prefabs/MeleeControllerMobile_NoInventory.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_Mobile/Shooter (Require Shooter Template)/Prefabs/ShooterControllerMobile_Inventory.prefab",
"Assets/Invector-3rdPersonController/Add-ons/Controller_Mobile/Shooter (Require Shooter Template)/Prefabs/ShooterControllerMobile_NoInventory.prefab"
};
private readonly struct Binding
{
public readonly string action;
public readonly bool isAxis;
public readonly GenericInputValueComponent component;
public readonly bool invert;
public Binding(string action, bool isAxis = false, GenericInputValueComponent component = GenericInputValueComponent.Scalar, bool invert = false)
{
this.action = action;
this.isAxis = isAxis;
this.component = component;
this.invert = invert;
}
}
///
/// Bindings keyed by (component type, field name), applied before so
/// components that reuse a generic field name (e.g. "input") for different purposes don't
/// collide with each other or with unrelated fields sharing that name.
///
private static readonly Dictionary<(string type, string field), Binding> typedBindings = new Dictionary<(string, string), Binding>
{
// The inventory grid is navigated with the D-Pad through the Navigate action, so both
// axes read the same Vector2 and pick the component they care about.
{ ("vInventory", "horizontal"), new Binding("Navigate", true, GenericInputValueComponent.X) },
{ ("vInventory", "vertical"), new Binding("Navigate", true, GenericInputValueComponent.Y) }
};
///
/// Bindings for GenericInput fields nested inside serialized lists, keyed by
/// (component type, list field, nested field). Applied per element of the list.
///
private static readonly Dictionary<(string type, string list, string field), Binding> nestedListBindings = new Dictionary<(string, string, string), Binding>
{
{ ("vInventory", "changeEquipmentControllers", "useItemInput"), new Binding("Use Item") }
};
private static readonly Dictionary bindings = new Dictionary
{
{ "horizontalInput", new Binding("Move", true, GenericInputValueComponent.X) },
{ "verticalInput", new Binding("Move", true, GenericInputValueComponent.Y) },
{ "rotateCameraXInput", new Binding("Look", true, GenericInputValueComponent.X) },
{ "rotateCameraYInput", new Binding("Look", true, GenericInputValueComponent.Y) },
{ "cameraZoomInput", new Binding("Zoom", true) },
{ "jumpInput", new Binding("Jump") },
{ "sprintInput", new Binding("Sprint") },
{ "crouchInput", new Binding("Crouch") },
{ "strafeInput", new Binding("Strafe") },
{ "rollInput", new Binding("Roll") },
{ "actionInput", new Binding("Interact") },
{ "enterInput", new Binding("Interact") },
{ "exitInput", new Binding("Jump") },
{ "fastClimbInput", new Binding("Sprint") },
{ "slideDownInput", new Binding("Roll") },
{ "weakAttackInput", new Binding("Weak Attack") },
{ "strongAttackInput", new Binding("Strong Attack") },
{ "blockInput", new Binding("Block") },
{ "lockOnInput", new Binding("Lock On") },
{ "previousTargetInput", new Binding("Previous Target", true) },
{ "nexTargetInput", new Binding("Next Target", true) },
{ "aimInput", new Binding("Aim") },
{ "shotInput", new Binding("Shoot") },
{ "reloadInput", new Binding("Reload") },
{ "scopeViewInput", new Binding("Scope") },
{ "switchCameraSideInput", new Binding("Switch Camera Side") },
{ "aimThrowInput", new Binding("Aim Throw") },
{ "throwInput", new Binding("Throw") },
{ "openInventory", new Binding("Toggle Inventory") },
{ "submit", new Binding("Submit") },
{ "cancel", new Binding("Cancel") },
{ "useItemInput", new Binding("Use Item") },
{ "removeEquipment", new Binding("Remove Equipment") },
{ "hideAndDrawWeaponsInput", new Binding("Draw Weapon") },
{ "pointClickInput", new Binding("Point Click") },
{ "rotateCameraInput", new Binding("Rotate Camera") },
{ "selectRightInput", new Binding("Toolbar Next") },
{ "selectLeftInput", new Binding("Toolbar Previous") }
};
///
/// vSimpleInput serves two opposite roles depending on where it is placed: a "press to
/// continue" prompt and a "press to go back" prompt. The role is only distinguishable from
/// the legacy key the prefab already authored, so it is resolved per component instead of
/// being fixed by type.
///
private static readonly Dictionary simpleInputActionByKey = new Dictionary
{
{ "Escape", "Cancel" },
{ "Backspace", "Cancel" },
{ "Return", "Continue" },
{ "KeypadEnter", "Continue" }
};
private const string SimpleInputFallbackAction = "Continue";
///
/// Slot hotkeys are bound by list position rather than by field name: element i of
/// vControlAreaByInput.slotsSelectors maps to the "Equip Slot (i+1)" action.
///
private const string SlotSelectorListField = "slotsSelectors";
private const string SlotSelectorInputField = "input";
private const string SlotActionPrefix = "Equip Slot ";
private static void MigrateBasicController()
{
MigratePrefab(BasicControllerPath);
}
private static void MigrateMeleeInventoryController()
{
MigratePrefab(MeleeInventoryControllerPath);
}
private static void MigrateMeleeNoInventoryController()
{
MigratePrefab(MeleeNoInventoryControllerPath);
}
private static void MigrateShooterMeleeInventoryController()
{
MigratePrefab(ShooterMeleeInventoryControllerPath);
}
private static void MigrateShooterMeleeNoInventoryController()
{
MigratePrefab(ShooterMeleeNoInventoryControllerPath);
}
private static void MigrateShooterOnlyInventoryController()
{
MigratePrefab(ShooterOnlyInventoryControllerPath);
}
private static void MigrateGameStyleSelectorMelee()
{
MigratePrefab(GameStyleSelectorMeleePath);
}
private static void MigrateGameStyleSelectorShooter()
{
MigratePrefab(GameStyleSelectorShooterPath);
}
private static void MigrateInventoryMeleeOnly()
{
MigratePrefab(InventoryMeleeOnlyPath);
}
private static void MigrateInventoryShooterMelee()
{
MigratePrefab(InventoryShooterMeleePath);
}
private static void MigrateInventoryShooterOnly()
{
MigratePrefab(InventoryShooterOnlyPath);
}
private static void MigrateAddonControllerPrefabs()
{
foreach (var prefabPath in AddonControllerPrefabPaths)
{
MigratePrefab(prefabPath);
}
}
private static void MigrateMobileControllerPrefabs()
{
foreach (var prefabPath in MobileControllerPrefabPaths)
{
MigratePrefab(prefabPath);
}
}
private static void MigratePrefab(string prefabPath)
{
var actions = AssetDatabase.LoadAssetAtPath(InputActionsPath);
if (actions == null)
{
Debug.LogError($"Input action asset was not found at '{InputActionsPath}'.");
return;
}
var prefabRoot = PrefabUtility.LoadPrefabContents(prefabPath);
try
{
if (HasMissingScripts(prefabRoot))
{
if (MigrateSerializedPrefabWithoutSaving(prefabPath, actions))
{
Debug.Log($"Migrated GenericInput fields in '{prefabPath}' directly because Unity cannot save its existing missing scripts.");
}
return;
}
ConfigureInputModules(prefabRoot);
var inputManager = prefabRoot.GetComponent();
if (inputManager == null) inputManager = prefabRoot.AddComponent();
var migrated = 0;
foreach (var component in prefabRoot.GetComponentsInChildren(true))
{
if (component == null) continue;
var serializedObject = new SerializedObject(component);
var changed = false;
var typeName = component.GetType().Name;
var handledFields = new HashSet();
foreach (var pair in typedBindings)
{
if (pair.Key.type != typeName) continue;
var property = serializedObject.FindProperty(pair.Key.field);
if (property == null || property.propertyType != SerializedPropertyType.Generic) continue;
if (!ConfigureGenericInput(property, pair.Value, actions)) continue;
handledFields.Add(pair.Key.field);
changed = true;
migrated++;
}
foreach (var pair in bindings)
{
if (handledFields.Contains(pair.Key)) continue;
var property = serializedObject.FindProperty(pair.Key);
if (property == null || property.propertyType != SerializedPropertyType.Generic) continue;
if (!ConfigureGenericInput(property, pair.Value, actions)) continue;
changed = true;
migrated++;
}
foreach (var pair in nestedListBindings)
{
if (pair.Key.type != typeName) continue;
var list = serializedObject.FindProperty(pair.Key.list);
if (list == null || !list.isArray) continue;
for (var i = 0; i < list.arraySize; i++)
{
var property = list.GetArrayElementAtIndex(i).FindPropertyRelative(pair.Key.field);
if (property == null || property.propertyType != SerializedPropertyType.Generic) continue;
if (!ConfigureGenericInput(property, pair.Value, actions)) continue;
changed = true;
migrated++;
}
}
if (typeName == "vInventory")
{
var controllers = serializedObject.FindProperty("changeEquipmentControllers");
if (controllers != null && controllers.isArray)
{
for (var i = 0; i < controllers.arraySize; i++)
{
var controller = controllers.GetArrayElementAtIndex(i);
migrated += ConfigureChangeEquipmentInput(controller, "previousItemInput", actions, ref changed);
migrated += ConfigureChangeEquipmentInput(controller, "nextItemInput", actions, ref changed);
}
}
}
if (typeName == "vSimpleInput")
{
var property = serializedObject.FindProperty("input");
if (property != null && property.propertyType == SerializedPropertyType.Generic)
{
var key = property.FindPropertyRelative("keyboard").stringValue;
if (!simpleInputActionByKey.TryGetValue(key, out var actionName)) actionName = SimpleInputFallbackAction;
if (ConfigureGenericInput(property, new Binding(actionName), actions))
{
changed = true;
migrated++;
}
}
}
if (typeName == "vControlAreaByInput")
{
var selectors = serializedObject.FindProperty(SlotSelectorListField);
if (selectors != null && selectors.isArray)
{
for (var i = 0; i < selectors.arraySize; i++)
{
var property = selectors.GetArrayElementAtIndex(i).FindPropertyRelative(SlotSelectorInputField);
if (property == null || property.propertyType != SerializedPropertyType.Generic) continue;
// Empty legacy selectors are display-only; assigning an action creates unintended hotkeys.
if (string.IsNullOrEmpty(property.FindPropertyRelative("keyboard").stringValue) &&
string.IsNullOrEmpty(property.FindPropertyRelative("joystick").stringValue) &&
string.IsNullOrEmpty(property.FindPropertyRelative("mobile").stringValue))
{
property.FindPropertyRelative("useInput").boolValue = false;
property.FindPropertyRelative("inputAction").objectReferenceValue = null;
changed = true;
continue;
}
var slotIndex = selectors.GetArrayElementAtIndex(i).FindPropertyRelative("indexOfSlot").intValue;
if (!ConfigureGenericInput(property, new Binding(SlotActionPrefix + (slotIndex + 1)), actions)) continue;
changed = true;
migrated++;
}
}
}
if (component is vThirdPersonInput)
{
var toggleWalkAction = serializedObject.FindProperty("toggleWalkAction");
if (toggleWalkAction != null)
{
toggleWalkAction.objectReferenceValue = GetReference(actions.FindAction("Toggle Walk", true));
changed = true;
}
}
if (changed) serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
AssetDatabase.SaveAssets();
var savedPrefab = PrefabUtility.SaveAsPrefabAsset(prefabRoot, prefabPath);
if (savedPrefab != null)
{
Debug.Log($"Migrated {migrated} GenericInput field(s) in '{prefabPath}' to the Input System.");
}
else if (MigrateSerializedPrefabWithoutSaving(prefabPath, actions))
{
Debug.Log($"Migrated GenericInput fields in '{prefabPath}' directly because Unity cannot save its existing missing scripts.");
}
}
finally
{
PrefabUtility.UnloadPrefabContents(prefabRoot);
}
}
private static bool ConfigureGenericInput(SerializedProperty input, Binding binding, InputActionAsset actions)
{
if (input == null) return false;
var action = actions.FindAction(binding.action, false);
if (action == null)
{
Debug.LogError($"Action '{binding.action}' is missing from '{InputActionsPath}'.");
return false;
}
input.FindPropertyRelative("inputSource").enumValueIndex = (int)GenericInputSource.InputSystem;
input.FindPropertyRelative("inputAction").objectReferenceValue = GetReference(action);
input.FindPropertyRelative("inputSystemAxis").boolValue = binding.isAxis;
input.FindPropertyRelative("inputSystemValueComponent").enumValueIndex = (int)binding.component;
input.FindPropertyRelative("inputSystemAxisInvert").boolValue = binding.invert;
return true;
}
private static void ConfigureInputModules(GameObject prefabRoot)
{
foreach (var eventSystem in prefabRoot.GetComponentsInChildren(true))
{
var backend = eventSystem.GetComponent();
if (backend == null) backend = eventSystem.gameObject.AddComponent();
backend.Configure();
}
}
private static int ConfigureChangeEquipmentInput(SerializedProperty controller, string fieldName, InputActionAsset actions, ref bool changed)
{
var input = controller.FindPropertyRelative(fieldName);
if (input == null || input.propertyType != SerializedPropertyType.Generic) return 0;
var joystick = input.FindPropertyRelative("joystick").stringValue;
var inverted = input.FindPropertyRelative("joystickAxisInvert").boolValue;
var actionName = ResolveDPadAction(joystick, inverted, fieldName == "previousItemInput");
if (!ConfigureGenericInput(input, new Binding(actionName), actions)) return 0;
changed = true;
return 1;
}
private static string ResolveDPadAction(string joystick, bool inverted, bool previous)
{
if (joystick == "D-Pad Vertical") return inverted ? "D-Pad Down" : "D-Pad Up";
if (joystick == "D-Pad Horizontal") return inverted ? "D-Pad Left" : "D-Pad Right";
return previous ? "D-Pad Left" : "D-Pad Right";
}
private static InputActionReference GetReference(InputAction action)
{
foreach (var asset in AssetDatabase.LoadAllAssetsAtPath(InputActionsPath))
{
if (asset is InputActionReference existingReference && existingReference.action != null && existingReference.action.id == action.id)
return existingReference;
}
var reference = InputActionReference.Create(action);
reference.name = action.name;
AssetDatabase.AddObjectToAsset(reference, InputActionsPath);
AssetDatabase.ImportAsset(InputActionsPath);
return reference;
}
///
/// Some legacy sample prefabs contain missing components and Unity refuses to save them, even
/// when the edited components are valid. This limited YAML fallback only updates GenericInput
/// data and leaves every missing component byte-for-byte untouched.
///
private static bool MigrateSerializedPrefabWithoutSaving(string prefabPath, InputActionAsset actions)
{
var fullPath = Path.GetFullPath(prefabPath);
var lines = File.ReadAllLines(fullPath);
var output = new List(lines.Length + 256);
var migrated = 0;
for (var i = 0; i < lines.Length; i++)
{
output.Add(lines[i]);
if (!TryGetGenericInputField(lines, i, out var fieldName, out var childIndent, out var blockEnd)) continue;
var binding = ResolveRawBinding(fieldName, lines, i, blockEnd);
if (binding == null) continue;
var action = actions.FindAction(binding.Value.action, false);
if (action == null) continue;
var reference = GetReference(action);
if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(reference, out var guid, out long localId)) continue;
// A field might already be in the Input System form if an interrupted migration is retried.
var alreadyMigrated = false;
for (var j = i + 1; j < blockEnd; j++)
{
if (lines[j].TrimStart().StartsWith("inputSource:"))
{
alreadyMigrated = true;
break;
}
}
if (alreadyMigrated) continue;
for (var j = i + 1; j < blockEnd; j++)
{
output.Add(lines[j]);
if (!lines[j].TrimStart().StartsWith("useInput:")) continue;
output.Add($"{childIndent}inputSource: {(int)GenericInputSource.InputSystem}");
output.Add($"{childIndent}inputAction: {{fileID: {localId}, guid: {guid}, type: 3}}");
output.Add($"{childIndent}inputSystemAxis: {(binding.Value.isAxis ? 1 : 0)}");
output.Add($"{childIndent}inputSystemAxisInvert: {(binding.Value.invert ? 1 : 0)}");
output.Add($"{childIndent}inputSystemValueComponent: {(int)binding.Value.component}");
output.Add($"{childIndent}inputSystemRawThreshold: 0.1");
}
i = blockEnd - 1;
migrated++;
}
if (migrated == 0) return false;
File.WriteAllText(fullPath, string.Join("\n", output) + "\n", new UTF8Encoding(false));
AssetDatabase.ImportAsset(prefabPath, ImportAssetOptions.ForceUpdate);
AssetDatabase.SaveAssets();
return true;
}
private static bool HasMissingScripts(GameObject prefabRoot)
{
foreach (var component in prefabRoot.GetComponentsInChildren(true))
{
if (component == null) return true;
}
return false;
}
private static bool TryGetGenericInputField(string[] lines, int lineIndex, out string fieldName, out string childIndent, out int blockEnd)
{
fieldName = null;
childIndent = null;
blockEnd = lineIndex;
var line = lines[lineIndex];
var colon = line.IndexOf(':');
if (colon <= 0 || line[(colon + 1)..].Trim().Length != 0) return false;
var parentIndentLength = line.Length - line.TrimStart().Length;
fieldName = line.Trim().TrimEnd(':');
if (fieldName.StartsWith("- ")) fieldName = fieldName[2..].TrimStart();
if (lineIndex + 1 >= lines.Length) return false;
childIndent = new string(' ', parentIndentLength + 2);
if (!lines[lineIndex + 1].StartsWith(childIndent + "useInput:")) return false;
blockEnd = lineIndex + 1;
while (blockEnd < lines.Length)
{
var candidate = lines[blockEnd];
if (candidate.Length > parentIndentLength && candidate.StartsWith(new string(' ', parentIndentLength + 1)))
{
blockEnd++;
continue;
}
break;
}
return true;
}
private static Binding? ResolveRawBinding(string fieldName, string[] lines, int blockStart, int blockEnd)
{
if (bindings.TryGetValue(fieldName, out var binding)) return binding;
if (fieldName == "horizontal") return new Binding("Navigate", true, GenericInputValueComponent.X);
if (fieldName == "vertical" || fieldName == "verticallInput") return new Binding(fieldName == "vertical" ? "Navigate" : "Move", true, fieldName == "vertical" ? GenericInputValueComponent.Y : GenericInputValueComponent.Y);
if (fieldName == "selectRightInput") return new Binding("Toolbar Next");
if (fieldName == "selectLeftInput") return new Binding("Toolbar Previous");
if (fieldName == "previousItemInput") return new Binding("D-Pad Left");
if (fieldName == "nextItemInput") return new Binding("D-Pad Right");
if (fieldName != "input") return null;
for (var i = blockStart + 1; i < blockEnd; i++)
{
var trimmed = lines[i].Trim();
if (!trimmed.StartsWith("keyboard:")) continue;
var key = trimmed["keyboard:".Length..].Trim();
return simpleInputActionByKey.TryGetValue(key, out var action) ? new Binding(action) : new Binding(SimpleInputFallbackAction);
}
return new Binding(SimpleInputFallbackAction);
}
}
}
#endif