Add Haptic feedback and selectable profiles

This commit is contained in:
Kamil Trzciński 2022-11-25 21:28:43 +01:00
parent d0b6fb93b0
commit bad617549e
15 changed files with 356 additions and 45 deletions

View file

@ -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<String> list)
{
return String.Join('\0', list);
}
public static String JoinWith0<TSource>(this IEnumerable<TSource> list, Func<TSource, String> 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<String> list)
{
return String.Join('\n', list);
}
public static String JoinWithN<TSource>(this IEnumerable<TSource> list, Func<TSource, String> selector)
{
return list.Select(selector).JoinWithN();
}
public static String[] SplitWithN(this String str)
{
return str.Split('\n', StringSplitOptions.RemoveEmptyEntries);
}
}
}

View file

@ -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;
}
}

View file

@ -9,14 +9,18 @@ using System.Threading.Tasks;
namespace CommonHelpers
{
public class SharedData<T> : IDisposable where T : unmanaged
public class SharedData<T> : 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<T>();
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<T> CreateNew(String? name = null)
{
return new SharedData<T>()
int size = AlignedSize();
return new SharedData<T>(size)
{
mmf = MemoryMappedFile.CreateOrOpen(name ?? GetUniqueName(), MMF_MAX_SIZE)
mmf = MemoryMappedFile.CreateOrOpen(name ?? GetUniqueName(), size)
};
}
public static SharedData<T> OpenExisting(String? name = null)
{
return new SharedData<T>()
int size = AlignedSize();
return new SharedData<T>(size)
{
mmf = MemoryMappedFile.OpenExisting(name ?? GetUniqueName())
};

View file

@ -455,6 +455,32 @@ namespace PowerControl
return null;
return selected;
}
},
new Menu.MenuItemWithOptions()
{
Name = "Controller",
ApplyDelay = 500,
OptionsValues = delegate()
{
if (SharedData<SteamControllerSetting>.GetExistingValue(out var value))
return value.SelectableProfiles.SplitWithN();
return null;
},
CurrentValue = delegate()
{
if (SharedData<SteamControllerSetting>.GetExistingValue(out var value))
return value.CurrentProfile.Length > 0 ? value.CurrentProfile : null;
return null;
},
ApplyValue = delegate(object selected)
{
if (!SharedData<SteamControllerSetting>.GetExistingValue(out var value))
return null;
value.DesiredProfile = (String)selected;
if (!SharedData<SteamControllerSetting>.SetExistingValue(value))
return null;
return selected;
}
}
}
};

View file

@ -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();
}
}
}

View file

@ -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<SteamControllerSetting> sharedData = SharedData<SteamControllerSetting>.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
{

View file

@ -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<SDCHapticPacket>()];
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;
}
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;

View file

@ -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)

View file

@ -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);

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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)
{

View file

@ -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);
}
}
}