From ecbd0407c092df2cc54e801d7fec854e3829773d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Thu, 24 Nov 2022 22:37:24 +0100 Subject: [PATCH] Add `SteamController` implementation This adds a Steam Shortcuts, Desktop Mode, and X360 Emulation - Supports all Steam Shortcuts (including on-screen keyboard, and brightness) - Supports Desktop mode (with a scroll on left pad and left stick), and trackpoint (on right stick) - Supports X360 mode: hold Options for 1s to switch between Desktop and X360 - Holding Steam button enables Desktop like controls and stops passing all inputs to X360 --- CommonHelpers/Log.cs | 16 + ExternalHelpers/OnScreenKeyboard.cs | 60 +++ ExternalHelpers/SDStructs.cs | 43 +- .../WindowsSettingsBrightnessController.cs | 8 + PowerControl/Controller.cs | 26 +- RELEASE.md | 4 + SteamController/Controller.cs | 131 ++++++ SteamController/Devices/KeyboardController.cs | 95 ++++ SteamController/Devices/MouseController.cs | 362 +++++++++++++++ SteamController/Devices/SteamButtons.cs | 76 ++++ SteamController/Devices/SteamController.cs | 65 +++ .../Devices/SteamControllerActions.cs | 421 ++++++++++++++++++ .../Devices/SteamControllerLizard.cs | 73 +++ SteamController/Devices/Xbox360Controller.cs | 160 +++++++ SteamController/Profiles/Context.cs | 91 ++++ SteamController/Profiles/DebugProfile.cs | 64 +++ SteamController/Profiles/DesktopProfile.cs | 112 +++++ SteamController/Profiles/Profile.cs | 19 + .../Profiles/SteamShortcutsProfile.cs | 166 +++++++ SteamController/Profiles/X360Profile.cs | 66 +++ SteamController/Program.cs | 21 + SteamController/Resources.Designer.cs | 123 +++++ SteamController/Resources.resx | 139 ++++++ .../microsoft-xbox-controller-off-red.ico | Bin 0 -> 195597 bytes .../microsoft-xbox-controller-off-red.png | Bin 0 -> 1323 bytes .../microsoft-xbox-controller-off-white.ico | Bin 0 -> 173192 bytes .../microsoft-xbox-controller-off-white.png | Bin 0 -> 920 bytes .../microsoft-xbox-controller-off.ico | Bin 0 -> 173370 bytes .../microsoft-xbox-controller-off.png | Bin 0 -> 839 bytes .../microsoft-xbox-controller-red.ico | Bin 0 -> 181853 bytes .../microsoft-xbox-controller-red.png | Bin 0 -> 942 bytes .../microsoft-xbox-controller-white.ico | Bin 0 -> 190574 bytes .../microsoft-xbox-controller-white.png | Bin 0 -> 2337 bytes .../Resources/microsoft-xbox-controller.ico | Bin 0 -> 168595 bytes .../Resources/microsoft-xbox-controller.png | Bin 0 -> 712 bytes SteamController/Settings.Designer.cs | 26 ++ SteamController/Settings.settings | 6 + SteamController/SteamController.csproj | 68 +++ SteamController/app.manifest | 61 +++ SteamDeckTools.sln | 16 +- VERSION | 2 +- 41 files changed, 2486 insertions(+), 34 deletions(-) create mode 100644 CommonHelpers/Log.cs create mode 100644 ExternalHelpers/OnScreenKeyboard.cs create mode 100644 SteamController/Controller.cs create mode 100644 SteamController/Devices/KeyboardController.cs create mode 100644 SteamController/Devices/MouseController.cs create mode 100644 SteamController/Devices/SteamButtons.cs create mode 100644 SteamController/Devices/SteamController.cs create mode 100644 SteamController/Devices/SteamControllerActions.cs create mode 100644 SteamController/Devices/SteamControllerLizard.cs create mode 100644 SteamController/Devices/Xbox360Controller.cs create mode 100644 SteamController/Profiles/Context.cs create mode 100644 SteamController/Profiles/DebugProfile.cs create mode 100644 SteamController/Profiles/DesktopProfile.cs create mode 100644 SteamController/Profiles/Profile.cs create mode 100644 SteamController/Profiles/SteamShortcutsProfile.cs create mode 100644 SteamController/Profiles/X360Profile.cs create mode 100644 SteamController/Program.cs create mode 100644 SteamController/Resources.Designer.cs create mode 100644 SteamController/Resources.resx create mode 100644 SteamController/Resources/microsoft-xbox-controller-off-red.ico create mode 100644 SteamController/Resources/microsoft-xbox-controller-off-red.png create mode 100644 SteamController/Resources/microsoft-xbox-controller-off-white.ico create mode 100644 SteamController/Resources/microsoft-xbox-controller-off-white.png create mode 100644 SteamController/Resources/microsoft-xbox-controller-off.ico create mode 100644 SteamController/Resources/microsoft-xbox-controller-off.png create mode 100644 SteamController/Resources/microsoft-xbox-controller-red.ico create mode 100644 SteamController/Resources/microsoft-xbox-controller-red.png create mode 100644 SteamController/Resources/microsoft-xbox-controller-white.ico create mode 100644 SteamController/Resources/microsoft-xbox-controller-white.png create mode 100644 SteamController/Resources/microsoft-xbox-controller.ico create mode 100644 SteamController/Resources/microsoft-xbox-controller.png create mode 100644 SteamController/Settings.Designer.cs create mode 100644 SteamController/Settings.settings create mode 100644 SteamController/SteamController.csproj create mode 100644 SteamController/app.manifest diff --git a/CommonHelpers/Log.cs b/CommonHelpers/Log.cs new file mode 100644 index 0000000..180af28 --- /dev/null +++ b/CommonHelpers/Log.cs @@ -0,0 +1,16 @@ +using System.Diagnostics; + +namespace CommonHelpers +{ + public static class Log + { + public static void TraceLine(string format, params object?[] arg) + { + String line = string.Format(format, arg); + + Trace.WriteLine(line); + if (Environment.UserInteractive) + Console.WriteLine(line); + } + } +} diff --git a/ExternalHelpers/OnScreenKeyboard.cs b/ExternalHelpers/OnScreenKeyboard.cs new file mode 100644 index 0000000..007d28a --- /dev/null +++ b/ExternalHelpers/OnScreenKeyboard.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Threading; + +namespace ExternalHelpers +{ + public static class OnScreenKeyboard + { + public const String TabTipPath = @"C:\Program Files\Common Files\Microsoft Shared\ink\TabTip.exe"; + + public static bool Toggle() + { + StartTabTip(); + + var type = Type.GetTypeFromCLSID(Guid.Parse("4ce576fa-83dc-4F88-951c-9d0782b4e376")); + if (type is null) + return false; + var instance = (ITipInvocation?)Activator.CreateInstance(type); + if (instance is null) + return false; + instance?.Toggle(GetDesktopWindow()); + Marshal.ReleaseComObject(instance); + return true; + } + + static void StartTabTip() + { + if (FindWindow("IPTIP_Main_Window", "") != IntPtr.Zero) + return; + + Process.Start(TabTipPath); + + for (int i = 0; i < 10 && FindWindow("IPTIP_Main_Window", "") == IntPtr.Zero; i++) + Thread.Sleep(100); + } + + [ComImport, Guid("4ce576fa-83dc-4F88-951c-9d0782b4e376")] + class UIHostNoLaunch + { + } + + [ComImport, Guid("37c994e7-432b-4834-a2f7-dce1f13b834b")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface ITipInvocation + { + void Toggle(IntPtr hwnd); + } + + [DllImport("user32.dll", SetLastError = false)] + static extern IntPtr GetDesktopWindow(); + + [DllImport("user32.dll", EntryPoint = "FindWindow")] + static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + } +} diff --git a/ExternalHelpers/SDStructs.cs b/ExternalHelpers/SDStructs.cs index 12707ff..d56dd92 100644 --- a/ExternalHelpers/SDStructs.cs +++ b/ExternalHelpers/SDStructs.cs @@ -45,7 +45,8 @@ namespace PowerControl.External CONFIGURE_BT = 0x18, } - public enum SDCButton0 + [Flags] + public enum SDCButton0 : ushort { BTN_L5 = 0b1000000000000000, BTN_OPTIONS = 0b0100000000000000, @@ -65,22 +66,25 @@ namespace PowerControl.External BTN_R2 = 0b0000000000000001, } - public enum SDCButton1 + [Flags] + public enum SDCButton1 : byte { BTN_LSTICK_PRESS = 0b01000000, + BTN_RPAD_TOUCH = 0b00010000, BTN_LPAD_TOUCH = 0b00001000, + BTN_RPAD_PRESS = 0b00000100, BTN_LPAD_PRESS = 0b00000010, - BTN_RPAD_PRESS = 0b00010000, - BTN_RPAD_TOUCH = 0b00000100, BTN_R5 = 0b00000001, } - public enum SDCButton2 + [Flags] + public enum SDCButton2 : byte { BTN_RSTICK_PRESS = 0b00000100, } - public enum SDCButton4 + [Flags] + public enum SDCButton4 : byte { BTN_LSTICK_TOUCH = 0b01000000, BTN_RSTICK_TOUCH = 0b10000000, @@ -88,29 +92,30 @@ namespace PowerControl.External BTN_L4 = 0b00000010, } - public enum SDCButton5 + [Flags] + public enum SDCButton5 : byte { BTN_QUICK_ACCESS = 0b00000100, } - [StructLayout(LayoutKind.Sequential)] public struct SDCInput { public byte ptype; //0x00 - public byte _a1; //0x01 - public byte _a2; //0x02 + public byte _a1; //0x01 + public byte _a2; //0x02 public byte _a3; //0x03 - public uint seq; //0x04 - public ushort buttons0; //0x09 - public byte buttons1; //0x0A - public byte buttons2; //0x0C - public byte buttons3; //0x0D - public byte buttons4; //0x0E - public byte buttons5; //0x0E + public uint seq; //0x04 + public SDCButton0 buttons0; //0x08 + public SDCButton1 buttons1; //0x0A + public SDCButton2 buttons2; //0x0B + public byte buttons3; //0x0C + public SDCButton4 buttons4; //0x0D + public SDCButton5 buttons5; //0x0E + public byte buttons6; //0x0F public short lpad_x; //0x10 public short lpad_y; //0x12 - public short rpad_x; //0x13 + public short rpad_x; //0x14 public short rpad_y; //0x16 public short accel_x; //0x18 public short accel_y; //0x1A @@ -136,7 +141,7 @@ namespace PowerControl.External var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); try { - return (SDCInput)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(SDCInput)); + return Marshal.PtrToStructure(handle.AddrOfPinnedObject()); } catch { diff --git a/ExternalHelpers/WindowsSettingsBrightnessController.cs b/ExternalHelpers/WindowsSettingsBrightnessController.cs index ef078d7..43cc906 100644 --- a/ExternalHelpers/WindowsSettingsBrightnessController.cs +++ b/ExternalHelpers/WindowsSettingsBrightnessController.cs @@ -12,6 +12,14 @@ namespace PowerControl.Helpers /// public static class WindowsSettingsBrightnessController { + public static void Increase(int brightness) + { + var current = Get(); + current += brightness; + current = Math.Clamp(current, 0, 100); + Set(current); + } + public static int Get(double roundValue = 10.0) { return (int)(Math.Round(Get() / roundValue) * roundValue); diff --git a/PowerControl/Controller.cs b/PowerControl/Controller.cs index e7cb84b..172122f 100644 --- a/PowerControl/Controller.cs +++ b/PowerControl/Controller.cs @@ -88,7 +88,7 @@ namespace PowerControl osdDismissTimer = new System.Windows.Forms.Timer(components); osdDismissTimer.Interval = 3000; - osdDismissTimer.Tick += delegate(object ? sender, EventArgs e) + osdDismissTimer.Tick += delegate (object? sender, EventArgs e) { hideOSD(); }; @@ -150,7 +150,7 @@ namespace PowerControl { GlobalHotKey.RegisterHotKey("VolumeUp", () => { - if ((neptuneDeviceState.buttons5 & (byte)SDCButton5.BTN_QUICK_ACCESS) != 0) + if (neptuneDeviceState.buttons5.HasFlag(SDCButton5.BTN_QUICK_ACCESS)) rootMenu.SelectNext("Brightness"); else rootMenu.SelectNext("Volume"); @@ -160,7 +160,7 @@ namespace PowerControl GlobalHotKey.RegisterHotKey("VolumeDown", () => { - if ((neptuneDeviceState.buttons5 & (byte)SDCButton5.BTN_QUICK_ACCESS) != 0) + if (neptuneDeviceState.buttons5.HasFlag(SDCButton5.BTN_QUICK_ACCESS)) rootMenu.SelectPrev("Brightness"); else rootMenu.SelectPrev("Volume"); @@ -207,7 +207,7 @@ namespace PowerControl } // Consume only some events to avoid under-running SWICD - if ((input.buttons5 & (byte)SDCButton5.BTN_QUICK_ACCESS) != 0) + if (neptuneDeviceState.buttons5.HasFlag(SDCButton5.BTN_QUICK_ACCESS)) Thread.Sleep(1000 / 30); else Thread.Sleep(250); @@ -230,12 +230,12 @@ namespace PowerControl return; // otherwise it did not yet trigger // Reset sequence: 3 dots + L4|R4|L5|R5 - if (input.buttons0 == (ushort)SDCButton0.BTN_L5 && - input.buttons1 == (byte)SDCButton1.BTN_R5 && + if (input.buttons0 == SDCButton0.BTN_L5 && + input.buttons1 == SDCButton1.BTN_R5 && input.buttons2 == 0 && input.buttons3 == 0 && - input.buttons4 == (byte)(SDCButton4.BTN_L4 | SDCButton4.BTN_R4) && - input.buttons5 == (byte)SDCButton5.BTN_QUICK_ACCESS) + input.buttons4 == (SDCButton4.BTN_L4 | SDCButton4.BTN_R4) && + input.buttons5 == SDCButton5.BTN_QUICK_ACCESS) { rootMenu.Show(); rootMenu.Reset(); @@ -243,7 +243,7 @@ namespace PowerControl return; } - if ((input.buttons5 & (byte)SDCButton5.BTN_QUICK_ACCESS) == 0 || !RTSS.IsOSDForeground()) + if (!neptuneDeviceState.buttons5.HasFlag(SDCButton5.BTN_QUICK_ACCESS) || !RTSS.IsOSDForeground()) { // schedule next repeat far in the future dismissNeptuneInput(); @@ -258,19 +258,19 @@ namespace PowerControl { return; } - else if (input.buttons0 == (ushort)SDCButton0.BTN_DPAD_LEFT) + else if (input.buttons0 == SDCButton0.BTN_DPAD_LEFT) { rootMenu.SelectPrev(); } - else if (input.buttons0 == (ushort)SDCButton0.BTN_DPAD_RIGHT) + else if (input.buttons0 == SDCButton0.BTN_DPAD_RIGHT) { rootMenu.SelectNext(); } - else if (input.buttons0 == (ushort)SDCButton0.BTN_DPAD_UP) + else if (input.buttons0 == SDCButton0.BTN_DPAD_UP) { rootMenu.Prev(); } - else if (input.buttons0 == (ushort)SDCButton0.BTN_DPAD_DOWN) + else if (input.buttons0 == SDCButton0.BTN_DPAD_DOWN) { rootMenu.Next(); } diff --git a/RELEASE.md b/RELEASE.md index 9632c27..691f435 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,3 +1,7 @@ +## 0.5.x + +- Introduce SteamController that provides 3 main modes of operation Desktop, X360 and Steam + ## 0.4.x - Highly risky: Allow to change CPU and GPU frequency (enable `EnableExperimentalFeatures` in `PowerControl.dll.config`) diff --git a/SteamController/Controller.cs b/SteamController/Controller.cs new file mode 100644 index 0000000..3de4f1e --- /dev/null +++ b/SteamController/Controller.cs @@ -0,0 +1,131 @@ +using CommonHelpers; +using ExternalHelpers; +using SteamController.Profiles; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace SteamController +{ + internal class Controller : IDisposable + { + public const String Title = "Steam Controller"; + public readonly String TitleWithVersion = Title + " v" + Application.ProductVersion.ToString(); + + Container components = new Container(); + NotifyIcon notifyIcon; + StartupManager startupManager = new StartupManager(Title); + + Context context; + Thread? contextThread; + bool running = true; + + [DllImport("sas.dll")] + static extern void SendSAS(bool asUser); + + public Controller() + { + Instance.RunOnce(TitleWithVersion, "Global\\SteamController"); + + SendSAS(true); + + context = new Context() + { + Profiles = { + new Profiles.SteamShortcutsProfile(), + new Profiles.DesktopProfile(), + new Profiles.X360Profile(), + new Profiles.DebugProfile() + } + }; + + var contextMenu = new ContextMenuStrip(components); + + if (startupManager.IsAvailable) + { + var startupItem = new ToolStripMenuItem("Run On Startup"); + startupItem.Checked = startupManager.Startup; + startupItem.Click += delegate + { + startupManager.Startup = !startupManager.Startup; + startupItem.Checked = startupManager.Startup; + }; + contextMenu.Items.Add(startupItem); + } + + var helpItem = contextMenu.Items.Add("&Help"); + helpItem.Click += delegate + { + System.Diagnostics.Process.Start("explorer.exe", "http://github.com/ayufan-research/steam-deck-tools"); + }; + + contextMenu.Items.Add(new ToolStripSeparator()); + + var exitItem = contextMenu.Items.Add("&Exit"); + exitItem.Click += delegate + { + Application.Exit(); + }; + + notifyIcon = new NotifyIcon(components); + notifyIcon.Icon = Resources.microsoft_xbox_controller_off; + notifyIcon.Text = TitleWithVersion; + notifyIcon.Visible = true; + notifyIcon.ContextMenuStrip = contextMenu; + + var contextStateUpdate = new System.Windows.Forms.Timer(components); + contextStateUpdate.Interval = 250; + contextStateUpdate.Enabled = true; + contextStateUpdate.Tick += ContextStateUpdate_Tick; + + contextThread = new Thread(ContextState_Update); + contextThread.Start(); + } + + private void ContextState_Update(object? obj) + { + while (running) + { + context.Update(); + } + } + + private void ContextStateUpdate_Tick(object? sender, EventArgs e) + { + context.X360.CreateClient(); + + if (!context.Mouse.Valid) + { + notifyIcon.Text = TitleWithVersion + ". Cannot send input"; + notifyIcon.Icon = Resources.microsoft_xbox_controller_off_red; + } + else if (!context.X360.Valid) + { + notifyIcon.Text = TitleWithVersion + ". Missing ViGEm?"; + notifyIcon.Icon = Resources.microsoft_xbox_controller_red; + } + else if (context.DesktopMode) + { + notifyIcon.Icon = Resources.microsoft_xbox_controller_off; + notifyIcon.Text = TitleWithVersion + ". Desktop mode"; + } + else + { + notifyIcon.Icon = Resources.microsoft_xbox_controller; + notifyIcon.Text = TitleWithVersion; + } + } + + public void Dispose() + { + running = false; + + if (contextThread != null) + { + contextThread.Interrupt(); + contextThread.Join(); + } + + using (context) { } + } + } +} diff --git a/SteamController/Devices/KeyboardController.cs b/SteamController/Devices/KeyboardController.cs new file mode 100644 index 0000000..f0629e1 --- /dev/null +++ b/SteamController/Devices/KeyboardController.cs @@ -0,0 +1,95 @@ +using WindowsInput; + +namespace SteamController.Devices +{ + public class KeyboardController : IDisposable + { + InputSimulator simulator = new InputSimulator(); + + HashSet keyCodes = new HashSet(); + HashSet lastKeyCodes = new HashSet(); + + public KeyboardController() + { + } + + public void Dispose() + { + } + + public bool this[VirtualKeyCode button] + { + get { return keyCodes.Contains(button); } + set + { + if (value) + keyCodes.Add(button); + else + keyCodes.Remove(button); + } + } + + public VirtualKeyCode[] DownKeys + { + get { return keyCodes.ToArray(); } + } + + internal void BeforeUpdate() + { + lastKeyCodes = keyCodes; + keyCodes = new HashSet(); + } + + internal void Update() + { + try + { + // Key Up: it is missing now + var keyUp = lastKeyCodes.Except(keyCodes).ToArray(); + if (keyUp.Any()) + simulator.Keyboard.KeyUp(keyUp); + + // Key Down: new keys being down + var keyDown = keyCodes.Except(lastKeyCodes).ToArray(); + if (keyDown.Any()) + simulator.Keyboard.KeyUp(keyDown); + } + catch (InvalidOperationException) + { + } + } + + public void KeyPress(params VirtualKeyCode[] keyCodes) + { + try + { + simulator.Keyboard.KeyPress(keyCodes); + } + catch (InvalidOperationException) + { + } + } + + public void KeyPress(VirtualKeyCode modifierKey, params VirtualKeyCode[] keyCodes) + { + try + { + simulator.Keyboard.ModifiedKeyStroke(modifierKey, keyCodes); + } + catch (InvalidOperationException) + { + } + } + + public void KeyPress(IEnumerable modifierKeys, params VirtualKeyCode[] keyCodes) + { + try + { + simulator.Keyboard.ModifiedKeyStroke(modifierKeys, keyCodes); + } + catch (InvalidOperationException) + { + } + } + } +} diff --git a/SteamController/Devices/MouseController.cs b/SteamController/Devices/MouseController.cs new file mode 100644 index 0000000..fdfbf3f --- /dev/null +++ b/SteamController/Devices/MouseController.cs @@ -0,0 +1,362 @@ +#define ACCUM_MOUSE +#define ACCUM_SCROLL + +using WindowsInput; +using static CommonHelpers.Log; + +namespace SteamController.Devices +{ + public class MouseController : IDisposable + { + private struct Accum + { + double? last, now; + + public bool Used + { + get { return now is not null; } + } + + public void Tick() + { + last = now; + now = null; + } + + public void Add(double delta) + { + now = (now ?? 0.0) + delta; + } + + public int Consume() + { + double accum = ((now ?? 0.0) + (last ?? 0.0)); + now = accum - (int)accum; + last = null; + return (int)accum; + } + } + + // TODO: Unsure what it is + public const int XButtonID = 0; + public const int YButtonID = 1; + public const int UpdateValidInterval = 250; + + InputSimulator simulator = new InputSimulator(); + + HashSet