From a610b86dada4f20e20dba57225af31adfb2ac296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Uriel=20Navall?= Date: Mon, 24 Nov 2025 10:46:33 -0300 Subject: [PATCH] Added a class to handle gamepad input to use with the freecam. --- src/Cinematic/IGamepadInputInterceptor.cs | 1106 +++++++++++++++++++++ src/UI/Panels/FreeCamPanel.cs | 71 +- 2 files changed, 1175 insertions(+), 2 deletions(-) create mode 100644 src/Cinematic/IGamepadInputInterceptor.cs diff --git a/src/Cinematic/IGamepadInputInterceptor.cs b/src/Cinematic/IGamepadInputInterceptor.cs new file mode 100644 index 0000000..7c22710 --- /dev/null +++ b/src/Cinematic/IGamepadInputInterceptor.cs @@ -0,0 +1,1106 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; +using UnityEngine; +using UnityExplorer; +using UnityExplorer.UI.Panels; + +// Uncomment to enable verbose logging in gamepad interceptor +// #define IGAMEPAD_DEBUG + +#if UNHOLLOWER +using IL2CPPUtils = UnhollowerBaseLib.UnhollowerUtils; +#endif +#if INTEROP +using IL2CPPUtils = Il2CppInterop.Common.Il2CppInteropUtils; +#endif + +namespace UniverseLib.Input +{ + /// + /// Gamepad input interceptor for modding. Captures gamepad input without direct type references, + /// allowing you to block game input while capturing it for your own use. + /// + public static class IGamepadInputInterceptor + { + // --- Configuration --- + private static bool _isEnabled = false; + private static int _targetGamepadIndex = 0; // 0 = first/current gamepad + + // --- Reflected types --- + private static Type _gamepadType; + private static Type _buttonControlType; + private static Type _axisControlType; + private static Type _vector2ControlType; + private static Type _inputControlType; + private static PropertyInfo _basePathProp; + + // --- Reflected properties and methods for triggering state capture --- + private static PropertyInfo _buttonIsPressedProp; + private static PropertyInfo _buttonWasPressedProp; + private static PropertyInfo _buttonWasReleasedProp; + private static MethodInfo _axisReadValueMethod; + private static MethodInfo _vector2ReadValueMethod; + + // --- Cached reflection for device checking --- + private static PropertyInfo _deviceProp; + + // --- Cached reflection for Vector2 property access --- + private static PropertyInfo _vector2XProp; + private static PropertyInfo _vector2YProp; + + // --- Cached reflection for gamepad control access --- + private static PropertyInfo _gamepadAllProp; + private static PropertyInfo _gamepadCurrentProp; + private static PropertyInfo _gamepadLeftStickProp; + private static PropertyInfo _gamepadRightStickProp; + + // --- Vendor name normalization --- + private static readonly Dictionary _vendorPrefixMap = new() + { + { "/xinputcontrollerwindows", "/gamepad" }, + { "/xinputcontroller", "/gamepad" }, + { "/dualshockgamepad", "/gamepad" }, + { "/dualsensegamepad", "/gamepad" }, + { "/dualshock4gamepadhid", "/gamepad" }, + { "/dualshock3gamepadhid", "/gamepad" }, + { "/androidgamepad", "/gamepad" }, + { "/switchprocontrollerhid", "/gamepad" }, + }; + + // --- Control collections --- + // Maps normalized paths to control objects + private static readonly Dictionary _buttonControls = new(); + private static readonly Dictionary _axisControls = new(); + private static readonly Dictionary _vector2Controls = new(); + + // --- State dictionaries (populated by postfix patches) --- + // Current frame states + private static readonly Dictionary _buttonPressedStates = new(); + private static readonly Dictionary _buttonWasPressedStates = new(); + private static readonly Dictionary _buttonWasReleasedStates = new(); + + // Analog values + private static readonly Dictionary _axisValues = new(); + private static readonly Dictionary _vector2Values = new(); + + // --- Initialization --- + + /// + /// Initializes the gamepad input interceptor. Must be called during game startup. + /// + public static void Init() + { + try + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning("[IGamepadInputInterceptor] === Initializing ==="); +#endif + + LoadReflectedTypes(); + if (_gamepadType == null || _buttonControlType == null) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning("[IGamepadInputInterceptor] Failed to load required types"); +#endif + return; + } + + PatchGamepadMethods(); + DiscoverGamepadControls(); + _isEnabled = true; + +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning("[IGamepadInputInterceptor] === Initialization complete ==="); +#endif + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Init failed: {ex.Message}\n{ex.StackTrace}"); + } + } + + /// + /// Enables input interception. When enabled, captured inputs will be replaced with default values. + /// + public static void Enable() + { + _isEnabled = true; +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning("[IGamepadInputInterceptor] Input interception enabled"); +#endif + } + + /// + /// Disables input interception. Game will receive normal gamepad input. + /// + public static void Disable() + { + _isEnabled = false; +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning("[IGamepadInputInterceptor] Input interception disabled"); +#endif + } + + /// + /// Sets which gamepad to target when multiple gamepads are connected. + /// + public static void SetTargetGamepad(int index) + { + _targetGamepadIndex = index; +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Target gamepad set to index {index}"); +#endif + // Re-discover controls for the new gamepad + ClearControlCaches(); + DiscoverGamepadControls(); + } + + // --- Type Loading --- + + private static void LoadReflectedTypes() + { + _gamepadType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Gamepad, Unity.InputSystem"); + _buttonControlType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Controls.ButtonControl, Unity.InputSystem"); + _axisControlType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Controls.AxisControl, Unity.InputSystem"); + _vector2ControlType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Controls.Vector2Control, Unity.InputSystem"); + _inputControlType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputControl, Unity.InputSystem"); + + _basePathProp = _inputControlType?.GetProperty("path", BindingFlags.Public | BindingFlags.Instance); + + _buttonIsPressedProp = _buttonControlType?.GetProperty("isPressed", BindingFlags.Public | BindingFlags.Instance); + _buttonWasPressedProp = _buttonControlType?.GetProperty("wasPressedThisFrame", BindingFlags.Public | BindingFlags.Instance); + _buttonWasReleasedProp = _buttonControlType?.GetProperty("wasReleasedThisFrame", BindingFlags.Public | BindingFlags.Instance); + + _axisReadValueMethod = _axisControlType?.GetMethod("ReadValue", Type.EmptyTypes); + _vector2ReadValueMethod = _vector2ControlType?.GetMethod("ReadValue", Type.EmptyTypes); + + _deviceProp = _inputControlType?.GetProperty("device", BindingFlags.Public | BindingFlags.Instance); + + _vector2XProp = _vector2ControlType?.GetProperty("x", BindingFlags.Public | BindingFlags.Instance); + _vector2YProp = _vector2ControlType?.GetProperty("y", BindingFlags.Public | BindingFlags.Instance); + + _gamepadAllProp = _gamepadType?.GetProperty("all", BindingFlags.Static | BindingFlags.Public); + _gamepadCurrentProp = _gamepadType?.GetProperty("current", BindingFlags.Static | BindingFlags.Public); + _gamepadLeftStickProp = _gamepadType?.GetProperty("leftStick", BindingFlags.Public | BindingFlags.Instance); + _gamepadRightStickProp = _gamepadType?.GetProperty("rightStick", BindingFlags.Public | BindingFlags.Instance); + +#if IGAMEPAD_DEBUG + LogLoadedType("Gamepad", _gamepadType); + LogLoadedType("ButtonControl", _buttonControlType); + LogLoadedType("AxisControl", _axisControlType); + LogLoadedType("Vector2Control", _vector2ControlType); +#endif + } + + private static void LogLoadedType(string name, Type type) + { + if (type != null) + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Loaded type: {name}"); + else + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to load type: {name}"); + } + + // --- Patching --- + + private static void PatchGamepadMethods() + { + try + { + if (_buttonControlType != null) + { + PatchPropertyGetter(_buttonControlType, "isPressed", + nameof(Postfix_ButtonIsPressed), null); + PatchPropertyGetter(_buttonControlType, "wasPressedThisFrame", + nameof(Postfix_ButtonWasPressed), null); + PatchPropertyGetter(_buttonControlType, "wasReleasedThisFrame", + nameof(Postfix_ButtonWasReleased), null); + + MethodInfo isValueConsideredPressedMethod = _buttonControlType.GetMethod("IsValueConsideredPressed", BindingFlags.Public | BindingFlags.Instance); + if (isValueConsideredPressedMethod != null) + { + PatchMethod(isValueConsideredPressedMethod, null, nameof(Postfix_ButtonIsValueConsideredPressed)); +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning("[IGamepadInputInterceptor] Patched ButtonControl.IsValueConsideredPressed()"); +#endif + } + } + + if (_vector2ControlType != null) + { + MethodInfo vector2ReadValueMethod = _vector2ControlType.GetMethod("ReadValue", Type.EmptyTypes); + if (vector2ReadValueMethod != null) + { + PatchMethod(vector2ReadValueMethod, null, nameof(Postfix_Vector2ReadValue)); +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning("[IGamepadInputInterceptor] Patched Vector2Control.ReadValue()"); +#endif + } + } + + if (_axisControlType != null) + { + // Patch AxisControl.ReadValue() method + MethodInfo readValueMethod = _axisControlType.GetMethod("ReadValue", Type.EmptyTypes); + if (readValueMethod != null) + { + PatchMethod(readValueMethod, null, nameof(Postfix_AxisReadValue)); +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning("[IGamepadInputInterceptor] Patched AxisControl.ReadValue()"); +#endif + } + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Error during patching: {ex.Message}"); + } + } + + private static void PatchPropertyGetter(Type type, string propertyName, string postfixName, string prefixName) + { + try + { + PropertyInfo prop = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); + if (prop?.GetGetMethod() == null) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Property not found: {type.Name}.{propertyName}"); + return; + } + +#if CPP + if (IL2CPPUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(prop.GetGetMethod()) == null) + return; +#endif + + MethodInfo getter = prop.GetGetMethod(); + MethodInfo postfixMethod = postfixName != null ? AccessTools.Method(typeof(IGamepadInputInterceptor), postfixName) : null; + MethodInfo prefixMethod = prefixName != null ? AccessTools.Method(typeof(IGamepadInputInterceptor), prefixName) : null; + + ExplorerCore.Harmony.Patch(getter, + prefix: prefixMethod != null ? new HarmonyMethod(prefixMethod) : null, + postfix: postfixMethod != null ? new HarmonyMethod(postfixMethod) : null); + +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Patched {type.Name}.{propertyName}"); +#endif + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to patch {type?.Name}.{propertyName}: {ex.Message}"); + } + } + + private static void PatchMethod(MethodInfo method, string prefixName, string postfixName) + { + try + { + if (method == null) + return; + +#if CPP + if (IL2CPPUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(method) == null) + return; +#endif + + MethodInfo prefixMethod = prefixName != null ? AccessTools.Method(typeof(IGamepadInputInterceptor), prefixName) : null; + MethodInfo postfixMethod = postfixName != null ? AccessTools.Method(typeof(IGamepadInputInterceptor), postfixName) : null; + + ExplorerCore.Harmony.Patch(method, + prefix: prefixMethod != null ? new HarmonyMethod(prefixMethod) : null, + postfix: postfixMethod != null ? new HarmonyMethod(postfixMethod) : null); + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to patch method: {ex.Message}"); + } + } + + // --- Control Discovery --- + + private static void DiscoverGamepadControls() + { + try + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning("[IGamepadInputInterceptor] Discovering gamepad controls..."); +#endif + + object gamepadInstance = GetGamepadInstance(_targetGamepadIndex); + if (gamepadInstance == null) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning("[IGamepadInputInterceptor] No gamepad found at specified index"); +#endif + return; + } + + // Instead of iterating allControls and trying to cast them, + // directly access the typed control properties on the Gamepad class. + // This gives us the controls with their correct types immediately. + + // List of known public control properties on Gamepad + string[] buttonProperties = new[] + { + "buttonSouth", "buttonNorth", "buttonWest", "buttonEast", + "leftStickButton", "rightStickButton", "leftShoulder", "rightShoulder", + "startButton", "selectButton", "leftTrigger", "rightTrigger" + }; + + string[] vector2Properties = new[] { "leftStick", "rightStick" }; + + // Register all button controls + foreach (string propName in buttonProperties) + { + try + { + PropertyInfo prop = _gamepadType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance); + if (prop != null) + { + object control = prop.GetValue(gamepadInstance, BindingFlags.Default, null, null, null); + if (control != null) + { + string normalizedPath = ExtractAndNormalizePath(control); + if (!string.IsNullOrEmpty(normalizedPath)) + { + _buttonControls[normalizedPath] = control; +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered button: {normalizedPath} ({propName})"); +#endif + } + } + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access button {propName}: {ex.Message}"); + } + } + + // Register D-pad individual direction buttons + try + { + PropertyInfo dpadProp = _gamepadType.GetProperty("dpad", BindingFlags.Public | BindingFlags.Instance); + if (dpadProp != null) + { + object dpadControl = dpadProp.GetValue(gamepadInstance, BindingFlags.Default, null, null, null); + if (dpadControl != null) + { + string[] dpadDirections = new[] { "up", "down", "left", "right" }; + foreach (string direction in dpadDirections) + { + try + { + PropertyInfo dpadDirProp = dpadControl.GetType().GetProperty(direction, BindingFlags.Public | BindingFlags.Instance); + if (dpadDirProp != null) + { + object buttonControl = dpadDirProp.GetValue(dpadControl, BindingFlags.Default, null, null, null); + if (buttonControl != null) + { + string buttonPath = ExtractAndNormalizePath(buttonControl); + if (!string.IsNullOrEmpty(buttonPath)) + { + _buttonControls[buttonPath] = buttonControl; +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered dpad button: {buttonPath} (dpad.{direction})"); +#endif + } + } + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access dpad.{direction}: {ex.Message}"); + } + } + } + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access dpad: {ex.Message}"); + } + + // Register all vector2 controls (sticks) + foreach (string propName in vector2Properties) + { + try + { + PropertyInfo prop = _gamepadType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance); + if (prop != null) + { + object control = prop.GetValue(gamepadInstance, BindingFlags.Default, null, null, null); + if (control != null) + { + string normalizedPath = ExtractAndNormalizePath(control); + if (!string.IsNullOrEmpty(normalizedPath)) + { + _vector2Controls[normalizedPath] = control; +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered vector2: {normalizedPath} ({propName})"); +#endif + } + + // Also register individual stick axes as axis controls + // Use the stick name to differentiate between left and right sticks + string[] axisNames = new[] { "x", "y" }; + foreach (string axisName in axisNames) + { + try + { + PropertyInfo axisProp = control.GetType().GetProperty(axisName, BindingFlags.Public | BindingFlags.Instance); + if (axisProp != null) + { + object axisControl = axisProp.GetValue(control, BindingFlags.Default, null, null, null); + if (axisControl != null) + { + string rawAxisPath = ExtractAndNormalizePath(axisControl); + if (!string.IsNullOrEmpty(rawAxisPath)) + { + // Inject the stick name into the path: /gamepad/x -> /gamepad/leftstick/x + string stickName = propName.ToLowerInvariant(); // "leftstick" or "rightstick" + string axisPath = $"/gamepad/{stickName}/{axisName}"; + _axisControls[axisPath] = axisControl; +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered stick axis: {axisPath} ({propName}.{axisName})"); +#endif + } + } + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access {propName}.{axisName}: {ex.Message}"); + } + } + } + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access vector2 {propName}: {ex.Message}"); + } + } + + // Register triggers as AXIS controls so analog values can be captured + // Triggers are ButtonControl but also support analog values + string[] triggerProperties = new[] { "leftTrigger", "rightTrigger" }; + foreach (string propName in triggerProperties) + { + try + { + PropertyInfo prop = _gamepadType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance); + if (prop != null) + { + object control = prop.GetValue(gamepadInstance, BindingFlags.Default, null, null, null); + if (control != null) + { + string normalizedPath = ExtractAndNormalizePath(control); + if (!string.IsNullOrEmpty(normalizedPath)) + { + // Add trigger axis path (e.g., /gamepad/lefttrigger for analog value) + _axisControls[normalizedPath] = control; +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered trigger axis: {normalizedPath} ({propName})"); +#endif + } + } + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access {propName}: {ex.Message}"); + } + } + + int totalControls = _buttonControls.Count + _axisControls.Count + _vector2Controls.Count; +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered {totalControls} controls " + + $"({_buttonControls.Count} buttons, {_axisControls.Count} axes, {_vector2Controls.Count} vectors)"); +#endif + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Error discovering controls: {ex.Message}\n{ex.StackTrace}"); + } + } + + private static object GetGamepadInstance(int index) + { + try + { + // Try to manually select the controller by index from the all collection + if (_gamepadAllProp != null) + { + object allGamepads = _gamepadAllProp.GetValue(null, BindingFlags.Default, null, null, null); + if (allGamepads != null) + { + Type collectionType = allGamepads.GetType(); + PropertyInfo countProp = collectionType.GetProperty("Count", BindingFlags.Public | BindingFlags.Instance); + PropertyInfo itemProp = collectionType.GetProperty("Item", BindingFlags.Public | BindingFlags.Instance); + + if (countProp != null && itemProp != null) + { + int count = (int)countProp.GetValue(allGamepads, BindingFlags.Default, null, null, null); + if (index >= 0 && index < count) + { + return itemProp.GetValue(allGamepads, new object[] { index }); + } + } + } + } + + // Fallback to Gamepad.current + if (_gamepadCurrentProp != null) + { + return _gamepadCurrentProp.GetValue(null, BindingFlags.Default, null, null, null); + } + + return null; + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Error getting gamepad instance: {ex.Message}"); + return null; + } + } + + // Some builds have problems when trying to retrieve property names, so we have to get some fallbacks into place while also trying to get a unified format. + private static string ExtractAndNormalizePath(object control) + { + if (control == null) + return null; + + string path = null; + + // Method 1: Try ToString() - safest on IL2CPP + try + { + string toString = control.ToString(); + if (!string.IsNullOrEmpty(toString)) + { + // Parse path from ToString (e.g., "ButtonControl{/xinputcontrollerwindows/buttonSouth}") + int slashIndex = toString.IndexOf('/'); + if (slashIndex >= 0) + { + int closeIndex = toString.IndexOf('}', slashIndex); + if (closeIndex >= 0) + { + path = toString.Substring(slashIndex, closeIndex - slashIndex); + } + } + } + } + catch { } + + // Method 2: Try reflection on path property (managed builds) +#if CPP + if (string.IsNullOrEmpty(path)) + { + try + { + if (_basePathProp != null) + { + object pathObj = _basePathProp.GetValue(control, BindingFlags.Default, null, null, null); + path = pathObj as string; + } + } + catch { } + } +#endif + + // Method 3: Try via name property (fallback) + if (string.IsNullOrEmpty(path)) + { + try + { + PropertyInfo nameProp = control.GetType().GetProperty("name", BindingFlags.Public | BindingFlags.Instance); + if (nameProp != null) + { + string controlName = nameProp.GetValue(control, BindingFlags.Default, null, null, null) as string; + if (!string.IsNullOrEmpty(controlName)) + { + path = $"/gamepad/{controlName}"; + } + } + } + catch { } + } + + if (string.IsNullOrEmpty(path)) + return null; + + return NormalizeControlPath(path); + } + + /// + /// Normalizes gamepad control paths to a common format across different controller types. + /// + private static string NormalizeControlPath(string rawPath) + { + if (string.IsNullOrEmpty(rawPath)) + return rawPath; + + string path = rawPath.ToLowerInvariant().Trim(); + + // Replace vendor-specific prefixes + foreach (var kvp in _vendorPrefixMap) + { + if (path.StartsWith(kvp.Key)) + { + path = kvp.Value + path.Substring(kvp.Key.Length); + break; + } + } + + return path; + } + + private static void ClearControlCaches() + { + _buttonControls.Clear(); + _axisControls.Clear(); + _vector2Controls.Clear(); + _buttonPressedStates.Clear(); + _buttonWasPressedStates.Clear(); + _buttonWasReleasedStates.Clear(); + _axisValues.Clear(); + _vector2Values.Clear(); + } + + // --- Public API --- + + /// + /// Gets whether a button is currently pressed. + /// + /// Normalized path like "/gamepad/buttonsouth" or vendor-specific path + public static bool IsButtonPressed(string buttonPath) + { + if (!_isEnabled) + return false; + + string normalized = NormalizeControlPath(buttonPath ?? ""); + +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] IsButtonPressed querying: {normalized}"); +#endif + + // Trigger the property getter to populate state + if (_buttonControls.TryGetValue(normalized, out object button) && _buttonIsPressedProp != null) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] IsButtonPressed found button, invoking getter"); +#endif + _buttonIsPressedProp.GetValue(button, BindingFlags.Default, null, null, null); + } +#if IGAMEPAD_DEBUG + else + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] IsButtonPressed NOT found in _buttonControls. Available: {string.Join(", ", _buttonControls.Keys)}"); + } +#endif + bool result = _buttonPressedStates.TryGetValue(normalized, out bool pressed) && pressed; +#if IGAMEPAD_DEBUG + //ExplorerCore.LogWarning($"[IGamepadInputInterceptor] IsButtonPressed returning: {result}"); +#endif + return result; + } + + /// + /// Gets whether a button was pressed this frame. + /// + public static bool WasButtonPressedThisFrame(string buttonPath) + { + if (!_isEnabled) + return false; + + string normalized = NormalizeControlPath(buttonPath ?? ""); + + // Trigger the property getter to populate state + if (_buttonControls.TryGetValue(normalized, out object button) && _buttonWasPressedProp != null) + { + _buttonWasPressedProp.GetValue(button, BindingFlags.Default, null, null, null); + } + + return _buttonWasPressedStates.TryGetValue(normalized, out bool pressed) && pressed; + } + + /// + /// Gets whether a button was released this frame. + /// + public static bool WasButtonReleasedThisFrame(string buttonPath) + { + if (!_isEnabled) + return false; + + string normalized = NormalizeControlPath(buttonPath ?? ""); + + // Trigger the property getter to populate state + if (_buttonControls.TryGetValue(normalized, out object button) && _buttonWasReleasedProp != null) + { + _buttonWasReleasedProp.GetValue(button, BindingFlags.Default, null, null, null); + } + + return _buttonWasReleasedStates.TryGetValue(normalized, out bool released) && released; + } + + /// + /// Gets the current value of an analog axis (trigger, stick axis, etc). + /// + /// Path like "/gamepad/lefttrigger" or "/gamepad/leftstick/x" + public static float GetAxisValue(string axisPath) + { + if (!_isEnabled) + return 0f; + + string normalized = NormalizeControlPath(axisPath ?? ""); +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] GetAxisValue querying: {normalized}"); +#endif + + // Trigger the ReadValue() method to populate state + if (_axisControls.TryGetValue(normalized, out object axis) && _axisReadValueMethod != null) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] GetAxisValue found axis, invoking ReadValue()"); +#endif + _axisReadValueMethod.Invoke(axis, null); + } +#if IGAMEPAD_DEBUG + else + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] GetAxisValue NOT found for: {normalized}. Available: {string.Join(", ", _axisControls.Keys)}"); + } +#endif + return _axisValues.TryGetValue(normalized, out float value) ? value : 0f; + } + + /// + /// Gets the current value of a 2D input (stick, dpad). + /// + /// Path like "/gamepad/leftstick" or "/gamepad/dpad" + public static Vector2 GetVector2Value(string vectorPath) + { + if (!_isEnabled) + return new Vector2(0f, 0f); + + string normalized = NormalizeControlPath(vectorPath ?? ""); + + // Trigger the ReadValue() method to populate state + if (_vector2Controls.TryGetValue(normalized, out object vector2) && _vector2ReadValueMethod != null) + { + _vector2ReadValueMethod.Invoke(vector2, null); + } + + return _vector2Values.TryGetValue(normalized, out var value) ? value : new Vector2(0f, 0f); + } + + /// + /// Gets all currently registered control paths. + /// + public static IEnumerable GetRegisteredButtonPaths() => _buttonControls.Keys; + + /// + /// Gets all currently registered axis paths. + /// + public static IEnumerable GetRegisteredAxisPaths() => _axisControls.Keys; + + /// + /// Gets all currently registered vector2 paths. + /// + public static IEnumerable GetRegisteredVector2Paths() => _vector2Controls.Keys; + + // --- Postfix Methods (capture state and optionally block) --- + + private static void Postfix_ButtonIsPressed(object __instance, ref bool __result) + { + if (__instance == null) + return; + + // Verify the button control is properly attached to a device before proceeding + try + { + if (_deviceProp != null) + { + object device = _deviceProp.GetValue(__instance, BindingFlags.Default, null, null, null); + if (device == null) + return; // Control not attached to a device, skip + } + } + catch + { + return; // If we can't check device, skip to be safe + } + + string path = ExtractAndNormalizePath(__instance); + if (!string.IsNullOrEmpty(path)) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_ButtonIsPressed: {path} = {__result}"); +#endif + _buttonPressedStates[path] = __result; + if (_isEnabled && FreeCamPanel.ShouldOverrideInput()) + __result = false; + } + } + + private static void Postfix_ButtonWasPressed(object __instance, ref bool __result) + { + if (__instance == null) + return; + + // Verify the button control is properly attached to a device before proceeding + try + { + if (_deviceProp != null) + { + object device = _deviceProp.GetValue(__instance, BindingFlags.Default, null, null, null); + if (device == null) + return; // Control not attached to a device, skip + } + } + catch + { + return; // If we can't check device, skip to be safe + } + + string path = ExtractAndNormalizePath(__instance); + if (!string.IsNullOrEmpty(path)) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_ButtonWasPressed: {path} = {__result}"); +#endif + _buttonWasPressedStates[path] = __result; + if (_isEnabled && FreeCamPanel.ShouldOverrideInput()) + __result = false; + } + } + + private static void Postfix_ButtonWasReleased(object __instance, ref bool __result) + { + if (__instance == null) + return; + + // Verify the button control is properly attached to a device before proceeding + try + { + if (_deviceProp != null) + { + object device = _deviceProp.GetValue(__instance, BindingFlags.Default, null, null, null); + if (device == null) + return; // Control not attached to a device, skip + } + } + catch + { + return; // If we can't check device, skip to be safe + } + + string path = ExtractAndNormalizePath(__instance); + if (!string.IsNullOrEmpty(path)) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_ButtonWasReleased: {path} = {__result}"); +#endif + _buttonWasReleasedStates[path] = __result; + // Block input if interception is enabled + if (_isEnabled && FreeCamPanel.ShouldOverrideInput()) + __result = false; + } + } + + private static void Postfix_Vector2ReadValue(object __instance, ref object __result) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_Vector2ReadValue called, instance={(__instance != null ? "not null" : "null")}, result={(__result != null ? "not null" : "null")}"); +#endif + if (__instance == null || __result == null) + return; + + // Verify the vector2 control is properly attached to a device before proceeding + try + { + if (_deviceProp != null) + { + object device = _deviceProp.GetValue(__instance, BindingFlags.Default, null, null, null); + if (device == null) + return; // Control not attached to a device, skip + } + } + catch + { + return; // If we can't check device, skip to be safe + } + + string path = ExtractAndNormalizePath(__instance); + if (!string.IsNullOrEmpty(path)) + { + try + { + if (_vector2XProp != null && _vector2YProp != null) + { + float x = (float)_vector2XProp.GetValue(__result, BindingFlags.Default, null, null, null); + float y = (float)_vector2YProp.GetValue(__result, BindingFlags.Default, null, null, null); + +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_Vector2ReadValue: {path} = ({x}, {y})"); +#endif + _vector2Values[path] = new Vector2(x, y); + + if (_isEnabled && FreeCamPanel.ShouldOverrideInput() && _vector2XProp.CanWrite && _vector2YProp.CanWrite) + { + _vector2XProp.SetValue(__result, 0f, BindingFlags.Default, null, null, null); + _vector2YProp.SetValue(__result, 0f, BindingFlags.Default, null, null, null); + } + } + } + catch (Exception ex) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Error in Postfix_Vector2ReadValue: {ex.Message}"); +#endif + } + } + } + + private static void Postfix_AxisReadValue(object __instance, ref float __result) + { + if (__instance == null) + return; + + // Verify the axis control is properly attached to a device before proceeding + try + { + if (_deviceProp != null) + { + object device = _deviceProp.GetValue(__instance, BindingFlags.Default, null, null, null); + if (device == null) + return; // Control not attached to a device, skip + } + } + catch + { + return; // If we can't check device, skip to be safe + } + + string path = ExtractAndNormalizePath(__instance); +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_AxisReadValue extracted path: {path}, value: {__result}"); +#endif + + // Try to identify if this axis belongs to a stick and which one by comparing the parent stick's axes + if (!string.IsNullOrEmpty(path)) + { + string adjustedPath = path; + + try + { + // Get the gamepad instance to access stick controls + object gamepadInstance = GetGamepadInstance(_targetGamepadIndex); + if (gamepadInstance != null) + { + // Check if this axis is part of leftStick + if (_gamepadLeftStickProp != null) + { + object leftStick = _gamepadLeftStickProp.GetValue(gamepadInstance, BindingFlags.Default, null, null, null); + if (leftStick != null) + { + PropertyInfo leftStickXProp = leftStick.GetType().GetProperty("x", BindingFlags.Public | BindingFlags.Instance); + PropertyInfo leftStickYProp = leftStick.GetType().GetProperty("y", BindingFlags.Public | BindingFlags.Instance); + + object leftStickX = leftStickXProp?.GetValue(leftStick, BindingFlags.Default, null, null, null); + object leftStickY = leftStickYProp?.GetValue(leftStick, BindingFlags.Default, null, null, null); + + // Compare by reference to see if __instance is the x or y of leftStick + if (__instance == leftStickX || __instance == leftStickY) + { + adjustedPath = path.Replace("/gamepad/x", "/gamepad/leftstick/x").Replace("/gamepad/y", "/gamepad/leftstick/y"); +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Identified as LEFT STICK axis, adjusted path: {adjustedPath}"); +#endif + } + } + } + + // If not left stick, check if it's part of rightStick + if (adjustedPath == path && _gamepadRightStickProp != null) + { + object rightStick = _gamepadRightStickProp.GetValue(gamepadInstance, BindingFlags.Default, null, null, null); + if (rightStick != null) + { + PropertyInfo rightStickXProp = rightStick.GetType().GetProperty("x", BindingFlags.Public | BindingFlags.Instance); + PropertyInfo rightStickYProp = rightStick.GetType().GetProperty("y", BindingFlags.Public | BindingFlags.Instance); + + object rightStickX = rightStickXProp?.GetValue(rightStick, BindingFlags.Default, null, null, null); + object rightStickY = rightStickYProp?.GetValue(rightStick, BindingFlags.Default, null, null, null); + + // Compare by reference to see if __instance is the x or y of rightStick + if (__instance == rightStickX || __instance == rightStickY) + { + adjustedPath = path.Replace("/gamepad/x", "/gamepad/rightstick/x").Replace("/gamepad/y", "/gamepad/rightstick/y"); +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Identified as RIGHT STICK axis, adjusted path: {adjustedPath}"); +#endif + } + } + } + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Error identifying stick axis: {ex.Message}"); + } + +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_AxisReadValue storing: {adjustedPath} = {__result}"); +#endif + _axisValues[adjustedPath] = __result; + // Block input if interception is enabled + if (_isEnabled && FreeCamPanel.ShouldOverrideInput()) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] BLOCKING Postfix_AxisReadValue: {adjustedPath} (was {__result}, now 0f)"); +#endif + __result = 0f; + } + } + } + + private static void Postfix_ButtonIsValueConsideredPressed(object __instance, ref bool __result) + { + if (__instance == null) + return; + + try + { + if (_deviceProp != null) + { + object device = _deviceProp.GetValue(__instance, BindingFlags.Default, null, null, null); + if (device == null) + return; + } + } + catch + { + return; + } + + string path = ExtractAndNormalizePath(__instance); + // We are not caching this state currently, just logging and blocking if needed + if (!string.IsNullOrEmpty(path)) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_ButtonIsValueConsideredPressed: {path} = {__result}"); +#endif + if (_isEnabled && FreeCamPanel.ShouldOverrideInput()) + { +#if IGAMEPAD_DEBUG + ExplorerCore.LogWarning($"[IGamepadInputInterceptor] BLOCKING Postfix_ButtonIsValueConsideredPressed: {path} (was {__result}, now false)"); +#endif + __result = false; + } + } + } + } +} diff --git a/src/UI/Panels/FreeCamPanel.cs b/src/UI/Panels/FreeCamPanel.cs index 5d2ad63..e80dcf9 100644 --- a/src/UI/Panels/FreeCamPanel.cs +++ b/src/UI/Panels/FreeCamPanel.cs @@ -1309,12 +1309,26 @@ namespace UnityExplorer.UI.Panels FreeCamPanel.currentUserCameraPosition = transform.position; FreeCamPanel.currentUserCameraRotation = transform.rotation; + var leftStickX = IGamepadInputInterceptor.GetAxisValue("/gamepad/leftstick/x"); + var leftStickY = IGamepadInputInterceptor.GetAxisValue("/gamepad/leftstick/y"); + var rightStickX = IGamepadInputInterceptor.GetAxisValue("/gamepad/rightstick/x"); + var rightStickY = IGamepadInputInterceptor.GetAxisValue("/gamepad/rightstick/y"); + float leftTrigger = IGamepadInputInterceptor.GetAxisValue("/gamepad/lefttrigger"); + float rightTrigger = IGamepadInputInterceptor.GetAxisValue("/gamepad/righttrigger"); + bool yButtonPressed = IGamepadInputInterceptor.IsButtonPressed("/gamepad/buttonnorth"); + bool xButtonPressed = IGamepadInputInterceptor.IsButtonPressed("/gamepad/buttonwest"); + bool dpadUpPressed = IGamepadInputInterceptor.IsButtonPressed("/gamepad/dpad/up"); + bool dpadDownPressed = IGamepadInputInterceptor.IsButtonPressed("/gamepad/dpad/down"); + bool dpadLeftPressed = IGamepadInputInterceptor.IsButtonPressed("/gamepad/dpad/left"); + bool dpadRightPressed = IGamepadInputInterceptor.IsButtonPressed("/gamepad/dpad/right"); + bool bButtonPressed = IGamepadInputInterceptor.IsButtonPressed("/gamepad/buttonsouth"); + float moveSpeed = FreeCamPanel.desiredMoveSpeed * 0.01665f; //"0.01665f" (60fps) in place of Time.DeltaTime. DeltaTime causes issues when game is paused. float speedModifier = 1; - if (IInputManager.GetKey(ConfigManager.Speed_Up_Movement.Value)) + if (IInputManager.GetKey(ConfigManager.Speed_Up_Movement.Value) || yButtonPressed) speedModifier = 10f; - if (IInputManager.GetKey(ConfigManager.Speed_Down_Movement.Value)) + if (IInputManager.GetKey(ConfigManager.Speed_Down_Movement.Value) || xButtonPressed) speedModifier = 0.1f; moveSpeed *= speedModifier; @@ -1337,6 +1351,59 @@ namespace UnityExplorer.UI.Panels if (IInputManager.GetKey(ConfigManager.Down.Value)) transform.position += transform.up * -1 * moveSpeed; + if (leftStickX != 0 || leftStickY != 0) + { + transform.position += transform.right * leftStickX * moveSpeed; + transform.position += transform.forward * leftStickY * moveSpeed; + } + + if (rightStickX != 0 || rightStickY != 0) + { + float rotationSpeed = moveSpeed * 30; + float newRotationX = transform.localEulerAngles.y + rightStickX * rotationSpeed; + float newRotationY = transform.localEulerAngles.x - rightStickY * rotationSpeed; + + // Block the camera rotation to not go further than looking directly up or down. + newRotationY = newRotationY > 180f ? Mathf.Clamp(newRotationY, 270f, 360f) : Mathf.Clamp(newRotationY, -1f, 90.0f); + + transform.localEulerAngles = new Vector3(newRotationY, newRotationX, transform.localEulerAngles.z); + } + + if (leftTrigger > 0) + { + transform.position += transform.up * leftTrigger * moveSpeed; + } + + if (rightTrigger > 0) + { + transform.position += transform.up * -rightTrigger * moveSpeed; + } + + if (dpadUpPressed) + { + FreeCamPanel.GetFreecam().fieldOfView += moveSpeed; + } + + if (dpadDownPressed) + { + FreeCamPanel.GetFreecam().fieldOfView -= moveSpeed; + } + + if (dpadLeftPressed) + { + transform.Rotate(0, 0, moveSpeed * 10, Space.Self); + } + + if (dpadRightPressed) + { + transform.Rotate(0, 0, -moveSpeed * 10, Space.Self); + } + + if (bButtonPressed) + { + FreeCamPanel.GetFreecam().fieldOfView = FreeCamPanel.currentCameraType == FreeCamPanel.FreeCameraType.New ? 60 : FreeCamPanel.originalCameraFOV; + } + // 90 degrees tilt when pressing the speed down hotkey if (IInputManager.GetKey(ConfigManager.Speed_Down_Movement.Value)) {