// Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 using System.Globalization; using GFramework.Game.Abstractions.Input; namespace GFramework.Godot.Input; /// /// 负责在 Godot 原生输入事件与框架绑定描述之间做双向转换。 /// internal static class GodotInputBindingCodec { /// /// 尝试把原生输入事件转换成框架绑定描述。 /// /// 原生输入事件。 /// 转换后的绑定描述。 /// 如果转换成功则返回 public static bool TryCreateBinding(InputEvent inputEvent, out InputBindingDescriptor binding) { ArgumentNullException.ThrowIfNull(inputEvent); switch (inputEvent) { case InputEventKey keyEvent: var keyCode = GetKeyCode(keyEvent); binding = new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.Key, FormattableString.Invariant($"key:{(int)keyCode}"), keyCode.ToString()); return true; case InputEventMouseButton mouseButtonEvent: binding = new InputBindingDescriptor( InputDeviceKind.KeyboardMouse, InputBindingKind.MouseButton, FormattableString.Invariant($"mouse:{(int)mouseButtonEvent.ButtonIndex}"), mouseButtonEvent.ButtonIndex.ToString()); return true; case InputEventJoypadButton joypadButtonEvent: binding = new InputBindingDescriptor( InputDeviceKind.Gamepad, InputBindingKind.GamepadButton, FormattableString.Invariant($"joy-button:{(int)joypadButtonEvent.ButtonIndex}"), joypadButtonEvent.ButtonIndex.ToString()); return true; case InputEventJoypadMotion joypadMotionEvent: var direction = joypadMotionEvent.AxisValue >= 0f ? 1f : -1f; binding = new InputBindingDescriptor( InputDeviceKind.Gamepad, InputBindingKind.GamepadAxis, FormattableString.Invariant($"joy-axis:{(int)joypadMotionEvent.Axis}:{direction.ToString(CultureInfo.InvariantCulture)}"), GetAxisDisplayName(joypadMotionEvent.Axis, direction), direction); return true; default: binding = null!; return false; } } /// /// 把框架绑定描述还原为 Godot 输入事件。 /// /// 绑定描述。 /// 可写回 `InputMap` 的输入事件。 /// 当绑定描述无法转换时抛出。 public static InputEvent CreateInputEvent(InputBindingDescriptor binding) { ArgumentNullException.ThrowIfNull(binding); return binding.BindingKind switch { InputBindingKind.Key => CreateKeyEvent(binding), InputBindingKind.MouseButton => CreateMouseButtonEvent(binding), InputBindingKind.GamepadButton => CreateGamepadButtonEvent(binding), InputBindingKind.GamepadAxis => CreateGamepadAxisEvent(binding), _ => throw new ArgumentException($"Unsupported binding kind '{binding.BindingKind}'.", nameof(binding)) }; } /// /// 从原生输入事件推断当前设备上下文。 /// /// 原生输入事件。 /// 推断出的设备上下文。 public static InputDeviceContext GetDeviceContext(InputEvent inputEvent) { ArgumentNullException.ThrowIfNull(inputEvent); return inputEvent switch { InputEventKey => new InputDeviceContext(InputDeviceKind.KeyboardMouse), InputEventMouse => new InputDeviceContext(InputDeviceKind.KeyboardMouse), InputEventJoypadButton joypadButtonEvent => CreateGamepadContext(joypadButtonEvent.Device), InputEventJoypadMotion joypadMotionEvent => CreateGamepadContext(joypadMotionEvent.Device), InputEventScreenTouch => new InputDeviceContext(InputDeviceKind.Touch), _ => new InputDeviceContext(InputDeviceKind.Unknown) }; } private static InputDeviceContext CreateGamepadContext(int deviceIndex) { return new InputDeviceContext( InputDeviceKind.Gamepad, deviceIndex, "gamepad"); } private static InputEventKey CreateKeyEvent(InputBindingDescriptor binding) { var code = ParseSingleSegment(binding.Code, "key"); return new InputEventKey { Keycode = (Key)code, PhysicalKeycode = (Key)code }; } private static InputEventMouseButton CreateMouseButtonEvent(InputBindingDescriptor binding) { var buttonIndex = ParseSingleSegment(binding.Code, "mouse"); return new InputEventMouseButton { ButtonIndex = (MouseButton)buttonIndex }; } private static InputEventJoypadButton CreateGamepadButtonEvent(InputBindingDescriptor binding) { var buttonIndex = ParseSingleSegment(binding.Code, "joy-button"); return new InputEventJoypadButton { ButtonIndex = (JoyButton)buttonIndex }; } private static InputEventJoypadMotion CreateGamepadAxisEvent(InputBindingDescriptor binding) { var parts = binding.Code.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 3 || !string.Equals(parts[0], "joy-axis", StringComparison.Ordinal)) { throw new ArgumentException($"Binding code '{binding.Code}' is not a valid joy-axis code.", nameof(binding)); } if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var axis)) { throw new ArgumentException($"Binding code '{binding.Code}' does not contain a valid axis index.", nameof(binding)); } if (!float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var direction)) { throw new ArgumentException($"Binding code '{binding.Code}' does not contain a valid axis direction.", nameof(binding)); } return new InputEventJoypadMotion { Axis = (JoyAxis)axis, AxisValue = direction }; } private static int ParseSingleSegment(string code, string prefix) { var parts = code.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2 || !string.Equals(parts[0], prefix, StringComparison.Ordinal)) { throw new ArgumentException($"Binding code '{code}' is not a valid {prefix} code.", nameof(code)); } if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) { throw new ArgumentException($"Binding code '{code}' does not contain a valid numeric value.", nameof(code)); } return value; } private static Key GetKeyCode(InputEventKey keyEvent) { return keyEvent.PhysicalKeycode != Key.None ? keyEvent.PhysicalKeycode : keyEvent.Keycode; } private static string GetAxisDisplayName(JoyAxis axis, float direction) { return direction >= 0f ? FormattableString.Invariant($"{axis} Positive") : FormattableString.Invariant($"{axis} Negative"); } }