From bad617549ebf3139b8a25b4641654c17fb906ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Fri, 25 Nov 2022 21:28:43 +0100 Subject: [PATCH] Add Haptic feedback and selectable profiles --- CommonHelpers/Extensions.cs | 42 ++++++++++ CommonHelpers/GlobalConfig.cs | 13 +++ CommonHelpers/SharedData.cs | 35 +++++--- PowerControl/MenuStack.cs | 26 ++++++ SteamController/Context.cs | 30 +++++-- SteamController/Controller.cs | 80 +++++++++++++------ .../Devices/SteamControllerHaptic.cs | 38 +++++++++ SteamController/Devices/Xbox360Controller.cs | 51 +++++++++++- ...ile.cs => DefaultGuideShortcutsProfile.cs} | 2 +- SteamController/Profiles/DesktopProfile.cs | 3 +- SteamController/Profiles/Profile.cs | 2 + SteamController/Profiles/SteamProfile.cs | 4 + .../Profiles/SteamWithShorcutsProfile.cs | 30 +++++++ SteamController/Profiles/X360Profile.cs | 2 +- SteamController/Profiles/X360RumbleProfile.cs | 43 ++++++++++ 15 files changed, 356 insertions(+), 45 deletions(-) create mode 100644 CommonHelpers/Extensions.cs create mode 100644 SteamController/Devices/SteamControllerHaptic.cs rename SteamController/Profiles/{SteamShortcutsProfile.cs => DefaultGuideShortcutsProfile.cs} (98%) create mode 100644 SteamController/Profiles/SteamWithShorcutsProfile.cs create mode 100644 SteamController/Profiles/X360RumbleProfile.cs diff --git a/CommonHelpers/Extensions.cs b/CommonHelpers/Extensions.cs new file mode 100644 index 0000000..17b3b16 --- /dev/null +++ b/CommonHelpers/Extensions.cs @@ -0,0 +1,42 @@ +using RTSSSharedMemoryNET; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CommonHelpers +{ + public static class Extensions + { + public static String JoinWith0(this IEnumerable list) + { + return String.Join('\0', list); + } + + public static String JoinWith0(this IEnumerable list, Func selector) + { + return list.Select(selector).JoinWith0(); + } + + public static String[] SplitWith0(this String str) + { + return str.Split('\0', StringSplitOptions.RemoveEmptyEntries); + } + + public static String JoinWithN(this IEnumerable list) + { + return String.Join('\n', list); + } + + public static String JoinWithN(this IEnumerable list, Func selector) + { + return list.Select(selector).JoinWithN(); + } + + public static String[] SplitWithN(this String str) + { + return str.Split('\n', StringSplitOptions.RemoveEmptyEntries); + } + } +} diff --git a/CommonHelpers/GlobalConfig.cs b/CommonHelpers/GlobalConfig.cs index e4d79ea..e25fa0f 100644 --- a/CommonHelpers/GlobalConfig.cs +++ b/CommonHelpers/GlobalConfig.cs @@ -53,4 +53,17 @@ namespace CommonHelpers { public PowerControlVisible Current; } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct SteamControllerSetting + { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public String CurrentProfile; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 2048)] + public String SelectableProfiles; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public String DesiredProfile; + } } diff --git a/CommonHelpers/SharedData.cs b/CommonHelpers/SharedData.cs index 3de8b88..72bbc1a 100644 --- a/CommonHelpers/SharedData.cs +++ b/CommonHelpers/SharedData.cs @@ -9,14 +9,18 @@ using System.Threading.Tasks; namespace CommonHelpers { - public class SharedData : IDisposable where T : unmanaged + public class SharedData : IDisposable where T : struct { - const int MMF_MAX_SIZE = 256; + const int MMF_MAX_SIZE = 16384; + const int MMF_ALIGN_SIZE = 256; private MemoryMappedFile mmf; + private int size; - private SharedData() - { } + private SharedData(int size) + { + this.size = size; + } public T NewValue() { @@ -32,7 +36,7 @@ namespace CommonHelpers if (!mmvStream.CanRead) return false; - byte[] buffer = new byte[MMF_MAX_SIZE]; + byte[] buffer = new byte[size]; mmvStream.Read(buffer, 0, buffer.Length); var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); @@ -63,7 +67,7 @@ namespace CommonHelpers if (!mmvStream.CanWrite) return false; - byte[] buffer = new byte[MMF_MAX_SIZE]; + byte[] buffer = new byte[size]; var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); try { @@ -126,17 +130,30 @@ namespace CommonHelpers return String.Format("Global_{0}_Setting", typeof(T).Name); } + private static int AlignedSize() + { + int size = Marshal.SizeOf(); + size = (size + MMF_ALIGN_SIZE - 1) / MMF_ALIGN_SIZE * MMF_ALIGN_SIZE; + if (size > MMF_MAX_SIZE) + throw new ArgumentException(); + return size; + } + public static SharedData CreateNew(String? name = null) { - return new SharedData() + int size = AlignedSize(); + + return new SharedData(size) { - mmf = MemoryMappedFile.CreateOrOpen(name ?? GetUniqueName(), MMF_MAX_SIZE) + mmf = MemoryMappedFile.CreateOrOpen(name ?? GetUniqueName(), size) }; } public static SharedData OpenExisting(String? name = null) { - return new SharedData() + int size = AlignedSize(); + + return new SharedData(size) { mmf = MemoryMappedFile.OpenExisting(name ?? GetUniqueName()) }; diff --git a/PowerControl/MenuStack.cs b/PowerControl/MenuStack.cs index cfd8781..667d9e1 100644 --- a/PowerControl/MenuStack.cs +++ b/PowerControl/MenuStack.cs @@ -455,6 +455,32 @@ namespace PowerControl return null; return selected; } + }, + new Menu.MenuItemWithOptions() + { + Name = "Controller", + ApplyDelay = 500, + OptionsValues = delegate() + { + if (SharedData.GetExistingValue(out var value)) + return value.SelectableProfiles.SplitWithN(); + return null; + }, + CurrentValue = delegate() + { + if (SharedData.GetExistingValue(out var value)) + return value.CurrentProfile.Length > 0 ? value.CurrentProfile : null; + return null; + }, + ApplyValue = delegate(object selected) + { + if (!SharedData.GetExistingValue(out var value)) + return null; + value.DesiredProfile = (String)selected; + if (!SharedData.SetExistingValue(value)) + return null; + return selected; + } } } }; diff --git a/SteamController/Context.cs b/SteamController/Context.cs index cabc28b..c58dae6 100644 --- a/SteamController/Context.cs +++ b/SteamController/Context.cs @@ -82,6 +82,19 @@ namespace SteamController } } + public Profiles.Profile? GetCurrentProfile() + { + foreach (var profile in OrderedProfiles) + { + if (profile.Selected(this)) + { + return profile; + } + } + + return null; + } + public bool Update() { Steam.BeforeUpdate(); @@ -91,13 +104,10 @@ namespace SteamController try { - foreach (var profile in OrderedProfiles) + var profile = GetCurrentProfile(); + if (profile is not null) { - if (profile.Selected(this)) - { - profile.Run(this); - break; - } + profile.Run(this); } return true; @@ -130,6 +140,8 @@ namespace SteamController var list = OrderedProfiles; list.Remove(profile); list.Insert(0, profile); + RequestDesktopMode = profile.IsDesktop; + Beep(); return true; } @@ -151,10 +163,16 @@ namespace SteamController list.Remove(profile); list.Insert(0, profile); + Beep(); return true; } return false; } + + public void Beep() + { + X360.Beep(); + } } } diff --git a/SteamController/Controller.cs b/SteamController/Controller.cs index 7724c23..2a24d44 100644 --- a/SteamController/Controller.cs +++ b/SteamController/Controller.cs @@ -19,9 +19,11 @@ namespace SteamController Context context = new Context() { Profiles = { - new Profiles.DesktopProfile(), - new Profiles.SteamProfile(), - new Profiles.X360Profile(), + new Profiles.DesktopProfile() { Name = "Desktop" }, + new Profiles.SteamProfile() { Name = "Steam", Visible = false }, + new Profiles.SteamWithShorcutsProfile() { Name = "Steam with Shortcuts", Visible = false }, + new Profiles.X360Profile() { Name = "X360" }, + new Profiles.X360RumbleProfile() { Name = "X360 with Rumble" } }, Managers = { new Managers.ProcessManager(), @@ -37,6 +39,8 @@ namespace SteamController TimeSpan lastUpdatesReset; readonly TimeSpan updateResetInterval = TimeSpan.FromSeconds(1); + SharedData sharedData = SharedData.CreateNew(); + public Controller() { Instance.RunOnce(TitleWithVersion, "Global\\SteamController"); @@ -48,12 +52,34 @@ namespace SteamController enabledItem.Click += delegate { context.RequestEnable = !context.RequestEnable; }; contextMenu.Opening += delegate { enabledItem.Checked = context.RequestEnable; }; contextMenu.Items.Add(enabledItem); + contextMenu.Items.Add(new ToolStripSeparator()); - var desktopModeItem = new ToolStripMenuItem("&Desktop Mode"); - desktopModeItem.Checked = context.RequestDesktopMode; - desktopModeItem.Click += delegate { context.RequestDesktopMode = !context.RequestDesktopMode; }; - contextMenu.Opening += delegate { desktopModeItem.Checked = context.RequestDesktopMode; }; - contextMenu.Items.Add(desktopModeItem); + foreach (var profile in context.Profiles) + { + if (profile.Name == "" || !profile.Visible) + continue; + + var profileItem = new ToolStripMenuItem(profile.Name); + profileItem.Click += delegate { lock (context) { context.SelectProfile(profile.Name); } }; + contextMenu.Opening += delegate { profileItem.Checked = context.GetCurrentProfile() == profile; }; + contextMenu.Items.Add(profileItem); + } + + contextMenu.Items.Add(new ToolStripSeparator()); + + var lizardMouseItem = new ToolStripMenuItem("Use Lizard &Mouse"); + lizardMouseItem.Checked = DefaultGuideShortcutsProfile.SteamModeLizardMouse; + lizardMouseItem.Click += delegate { DefaultGuideShortcutsProfile.SteamModeLizardMouse = !DefaultGuideShortcutsProfile.SteamModeLizardMouse; }; + contextMenu.Opening += delegate { lizardMouseItem.Checked = DefaultGuideShortcutsProfile.SteamModeLizardMouse; }; + contextMenu.Items.Add(lizardMouseItem); + + var lizardButtonsItem = new ToolStripMenuItem("Use Lizard &Buttons"); + lizardButtonsItem.Checked = DefaultGuideShortcutsProfile.SteamModeLizardButtons; + lizardButtonsItem.Click += delegate { DefaultGuideShortcutsProfile.SteamModeLizardButtons = !DefaultGuideShortcutsProfile.SteamModeLizardButtons; }; + contextMenu.Opening += delegate { lizardButtonsItem.Checked = DefaultGuideShortcutsProfile.SteamModeLizardButtons; }; + contextMenu.Items.Add(lizardButtonsItem); + + contextMenu.Items.Add(new ToolStripSeparator()); var steamDetectionItem = new ToolStripMenuItem("Auto-disable on &Steam"); steamDetectionItem.Checked = Settings.Default.EnableSteamDetection; @@ -64,21 +90,6 @@ namespace SteamController }; contextMenu.Opening += delegate { steamDetectionItem.Checked = Settings.Default.EnableSteamDetection; }; contextMenu.Items.Add(steamDetectionItem); - contextMenu.Items.Add(new ToolStripSeparator()); - - var lizardMouseItem = new ToolStripMenuItem("Use Lizard &Mouse"); - lizardMouseItem.Checked = SteamShortcutsProfile.SteamModeLizardMouse; - lizardMouseItem.Click += delegate { SteamShortcutsProfile.SteamModeLizardMouse = !SteamShortcutsProfile.SteamModeLizardMouse; }; - contextMenu.Opening += delegate { lizardMouseItem.Checked = SteamShortcutsProfile.SteamModeLizardMouse; }; - contextMenu.Items.Add(lizardMouseItem); - - var lizardButtonsItem = new ToolStripMenuItem("Use Lizard &Buttons"); - lizardButtonsItem.Checked = SteamShortcutsProfile.SteamModeLizardButtons; - lizardButtonsItem.Click += delegate { SteamShortcutsProfile.SteamModeLizardButtons = !SteamShortcutsProfile.SteamModeLizardButtons; }; - contextMenu.Opening += delegate { lizardButtonsItem.Checked = SteamShortcutsProfile.SteamModeLizardButtons; }; - contextMenu.Items.Add(lizardButtonsItem); - - contextMenu.Items.Add(new ToolStripSeparator()); if (startupManager.IsAvailable) { @@ -139,6 +150,23 @@ namespace SteamController } } + private void SharedData_Update() + { + if (sharedData.GetValue(out var value) && value.DesiredProfile != "") + { + lock (context) + { + context.SelectProfile(value.DesiredProfile); + } + } + + sharedData.SetValue(new SteamControllerSetting() + { + CurrentProfile = context.Profiles.FirstOrDefault((profile) => profile.Selected(context))?.Name, + SelectableProfiles = context.Profiles.Where((profile) => profile.Selected(context) || profile.Visible).JoinWithN((profile) => profile.Name), + }); + } + private void ContextStateUpdate_Tick(object? sender, EventArgs e) { lock (context) @@ -146,6 +174,8 @@ namespace SteamController context.Tick(); } + SharedData_Update(); + if (!context.Mouse.Valid) { notifyIcon.Text = TitleWithVersion + ". Cannot send input."; @@ -165,6 +195,10 @@ namespace SteamController { notifyIcon.Icon = context.DesktopMode ? Resources.monitor : Resources.microsoft_xbox_controller; notifyIcon.Text = TitleWithVersion; + + var profile = context.GetCurrentProfile(); + if (profile is not null) + notifyIcon.Text = TitleWithVersion + ". Profile: " + profile.Name; } else { diff --git a/SteamController/Devices/SteamControllerHaptic.cs b/SteamController/Devices/SteamControllerHaptic.cs new file mode 100644 index 0000000..2d72533 --- /dev/null +++ b/SteamController/Devices/SteamControllerHaptic.cs @@ -0,0 +1,38 @@ +using System.Runtime.InteropServices; +using CommonHelpers; +using hidapi; +using PowerControl.External; +using static CommonHelpers.Log; + +namespace SteamController.Devices +{ + public partial class SteamController + { + public bool SetHaptic(byte position, ushort amplitude, ushort period, ushort count = 0) + { + var haptic = new SDCHapticPacket() + { + packet_type = (byte)SDCPacketType.PT_FEEDBACK, + len = (byte)SDCPacketLength.PL_FEEDBACK, + position = position, + amplitude = amplitude, + period = period, + count = count + }; + + var bytes = new byte[Marshal.SizeOf()]; + var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + try + { + Marshal.StructureToPtr(haptic, handle.AddrOfPinnedObject(), false); + neptuneDevice.RequestFeatureReport(bytes); + return true; + } + catch (Exception e) + { + Log.TraceLine("STEAM: Haptic: Exception: {0}", e); + return false; + } + } + } +} diff --git a/SteamController/Devices/Xbox360Controller.cs b/SteamController/Devices/Xbox360Controller.cs index 9823184..d663540 100644 --- a/SteamController/Devices/Xbox360Controller.cs +++ b/SteamController/Devices/Xbox360Controller.cs @@ -8,6 +8,8 @@ namespace SteamController.Devices { public class Xbox360Controller : IDisposable { + public readonly TimeSpan FeedbackTimeout = TimeSpan.FromMilliseconds(1000); + private ViGEmClient? client; private IXbox360Controller? device; private bool isConnected; @@ -49,8 +51,9 @@ namespace SteamController.Devices if (!isConnected) { - FeedbackLargeMotor = 0; - FeedbackSmallMotor = 0; + FeedbackLargeMotor = null; + FeedbackSmallMotor = null; + FeedbackReceived = null; LedNumber = 0; } @@ -77,6 +80,32 @@ namespace SteamController.Devices isConnected = Connected; } + internal void Disconnect() + { + if (!isConnected) + return; + + device?.Disconnect(); + isConnected = false; + } + + internal void Beep() + { + // TODO: reconnect to beep + if (isConnected) + { + device?.Disconnect(); + Thread.Sleep(100); + device?.Connect(); + } + else + { + device?.Connect(); + Thread.Sleep(100); + device?.Disconnect(); + } + } + internal void Update() { UpdateConnected(); @@ -85,6 +114,13 @@ namespace SteamController.Devices { device?.SubmitReport(); } + + if (FeedbackReceived is not null && FeedbackReceived.Value.Add(FeedbackTimeout) < DateTime.Now) + { + FeedbackLargeMotor = null; + FeedbackSmallMotor = null; + FeedbackReceived = null; + } } public bool Valid @@ -93,9 +129,10 @@ namespace SteamController.Devices } public bool Connected { get; set; } - public byte FeedbackLargeMotor { get; internal set; } - public byte FeedbackSmallMotor { get; internal set; } + public byte? FeedbackLargeMotor { get; internal set; } + public byte? FeedbackSmallMotor { get; internal set; } public byte LedNumber { get; internal set; } + public DateTime? FeedbackReceived { get; set; } public bool this[Xbox360Button button] { @@ -150,11 +187,17 @@ namespace SteamController.Devices submitReport = true; } + public void ResetFeedback() + { + FeedbackReceived = null; + } + private void X360Device_FeedbackReceived(object sender, Xbox360FeedbackReceivedEventArgs e) { FeedbackLargeMotor = e.LargeMotor; FeedbackSmallMotor = e.SmallMotor; LedNumber = e.LedNumber; + FeedbackReceived = DateTime.Now; } } } diff --git a/SteamController/Profiles/SteamShortcutsProfile.cs b/SteamController/Profiles/DefaultGuideShortcutsProfile.cs similarity index 98% rename from SteamController/Profiles/SteamShortcutsProfile.cs rename to SteamController/Profiles/DefaultGuideShortcutsProfile.cs index 6870961..64ceead 100644 --- a/SteamController/Profiles/SteamShortcutsProfile.cs +++ b/SteamController/Profiles/DefaultGuideShortcutsProfile.cs @@ -6,7 +6,7 @@ using WindowsInput; namespace SteamController.Profiles { - public abstract class SteamShortcutsProfile : DefaultShortcutsProfile + public abstract class DefaultGuideShortcutsProfile : DefaultShortcutsProfile { public static bool SteamModeLizardButtons = false; public static bool SteamModeLizardMouse = true; diff --git a/SteamController/Profiles/DesktopProfile.cs b/SteamController/Profiles/DesktopProfile.cs index 3db45e0..ab912d0 100644 --- a/SteamController/Profiles/DesktopProfile.cs +++ b/SteamController/Profiles/DesktopProfile.cs @@ -2,12 +2,13 @@ using WindowsInput; namespace SteamController.Profiles { - public sealed class DesktopProfile : SteamShortcutsProfile + public sealed class DesktopProfile : DefaultGuideShortcutsProfile { private const String Consumed = "DesktopProfileOwner"; public DesktopProfile() { + IsDesktop = true; } public override bool Selected(Context context) diff --git a/SteamController/Profiles/Profile.cs b/SteamController/Profiles/Profile.cs index 7f71008..07a7c97 100644 --- a/SteamController/Profiles/Profile.cs +++ b/SteamController/Profiles/Profile.cs @@ -11,6 +11,8 @@ namespace SteamController.Profiles } public String Name { get; set; } = ""; + public bool Visible { get; set; } = true; + public bool IsDesktop { get; set; } public abstract bool Selected(Context context); diff --git a/SteamController/Profiles/SteamProfile.cs b/SteamController/Profiles/SteamProfile.cs index 9e5d731..70c7615 100644 --- a/SteamController/Profiles/SteamProfile.cs +++ b/SteamController/Profiles/SteamProfile.cs @@ -4,6 +4,10 @@ namespace SteamController.Profiles { public sealed class SteamProfile : DefaultShortcutsProfile { + public SteamProfile() + { + } + public override bool Selected(Context context) { return context.Enabled && context.SteamUsesController; diff --git a/SteamController/Profiles/SteamWithShorcutsProfile.cs b/SteamController/Profiles/SteamWithShorcutsProfile.cs new file mode 100644 index 0000000..bf57efe --- /dev/null +++ b/SteamController/Profiles/SteamWithShorcutsProfile.cs @@ -0,0 +1,30 @@ +using Nefarius.ViGEm.Client.Targets.Xbox360; + +namespace SteamController.Profiles +{ + public sealed class SteamWithShorcutsProfile : DefaultGuideShortcutsProfile + { + public SteamWithShorcutsProfile() + { + } + + public override bool Selected(Context context) + { + return context.Enabled && context.SteamUsesController; + } + + public override Status Run(Context context) + { + // Steam does not use Lizard + context.Steam.LizardButtons = false; + context.Steam.LizardMouse = false; + + if (base.Run(context).IsDone) + { + return Status.Done; + } + + return Status.Continue; + } + } +} diff --git a/SteamController/Profiles/X360Profile.cs b/SteamController/Profiles/X360Profile.cs index 62c297f..b4a70b8 100644 --- a/SteamController/Profiles/X360Profile.cs +++ b/SteamController/Profiles/X360Profile.cs @@ -2,7 +2,7 @@ using Nefarius.ViGEm.Client.Targets.Xbox360; namespace SteamController.Profiles { - public sealed class X360Profile : SteamShortcutsProfile + public class X360Profile : DefaultGuideShortcutsProfile { public override bool Selected(Context context) { diff --git a/SteamController/Profiles/X360RumbleProfile.cs b/SteamController/Profiles/X360RumbleProfile.cs new file mode 100644 index 0000000..ed3ac64 --- /dev/null +++ b/SteamController/Profiles/X360RumbleProfile.cs @@ -0,0 +1,43 @@ +using CommonHelpers; +using Nefarius.ViGEm.Client.Targets.Xbox360; + +namespace SteamController.Profiles +{ + public class X360RumbleProfile : X360Profile + { + public const ushort FeedbackMaxAmplitude = 255; + public const ushort FeedbackPeriod = 10; + public const ushort FeedbackCount = 1; + + public override Status Run(Context context) + { + if (base.Run(context).IsDone) + { + return Status.Done; + } + + if (context.X360.FeedbackLargeMotor.HasValue) + { + Log.TraceLine("X360: Feedback Large: {0}", context.X360.FeedbackLargeMotor.Value); + context.Steam.SetHaptic( + 1, GetHapticAmplitude(context.X360.FeedbackLargeMotor), FeedbackPeriod, FeedbackCount); + context.X360.FeedbackLargeMotor = null; + } + + if (context.X360.FeedbackSmallMotor.HasValue) + { + Log.TraceLine("X360: Feedback Small: {0}", context.X360.FeedbackSmallMotor.Value); + context.Steam.SetHaptic( + 0, GetHapticAmplitude(context.X360.FeedbackSmallMotor), FeedbackPeriod, FeedbackCount); + context.X360.FeedbackSmallMotor = null; + } + + return Status.Continue; + } + + private ushort GetHapticAmplitude(byte? value) + { + return (ushort)(FeedbackMaxAmplitude * (value ?? 0) / byte.MaxValue); + } + } +}