#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