From e5debff45bc39374a0111076d7f86cf667c4b0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Sat, 10 Dec 2022 14:39:42 +0100 Subject: [PATCH] SteamController: Use `Roslyn Scripting` to compile `UserProfiles` - This looks into `UserProfiles/` and compiles user profiles - This exposes a very minimal scripting interface as defined by `Dynamic.Globals` --- SteamController/Controller.cs | 49 +++- SteamController/Devices/SteamAction.cs | 2 +- SteamController/Devices/SteamController.cs | 1 + SteamController/Profiles/Dynamic/Globals.cs | 244 ++++++++++++++++++ .../Profiles/Dynamic/RoslynDynamicProfile.cs | 186 +++++++++++++ SteamController/Profiles/Profile.cs | 13 + SteamController/SteamController.csproj | 11 + SteamController/UserProfiles/Turbo.x360.cs | 9 + 8 files changed, 504 insertions(+), 11 deletions(-) create mode 100644 SteamController/Profiles/Dynamic/Globals.cs create mode 100644 SteamController/Profiles/Dynamic/RoslynDynamicProfile.cs create mode 100644 SteamController/UserProfiles/Turbo.x360.cs diff --git a/SteamController/Controller.cs b/SteamController/Controller.cs index 93484b0..809e6fb 100644 --- a/SteamController/Controller.cs +++ b/SteamController/Controller.cs @@ -10,6 +10,12 @@ namespace SteamController public const String Title = "Steam Controller"; public static readonly String TitleWithVersion = Title + " v" + Application.ProductVersion.ToString(); + public static readonly Dictionary PreconfiguredUserProfiles = new Dictionary() + { + { "*.desktop.cs", new Profiles.Predefined.DesktopProfile() { Name = "Desktop" } }, + { "*.x360.cs", new Profiles.Predefined.X360HapticProfile() { Name = "X360" } } + }; + Container components = new Container(); NotifyIcon notifyIcon; StartupManager startupManager = new StartupManager(Title); @@ -55,17 +61,39 @@ namespace SteamController startupManager.Startup = false; }); - // Set available profiles - ProfilesSettings.Helpers.ProfileStringConverter.Profiles = context.Profiles. - Where((profile) => profile.Visible). - Select((profile) => profile.Name).ToArray(); - Instance.RunOnce(TitleWithVersion, "Global\\SteamController"); Instance.RunUpdater(TitleWithVersion); if (Instance.WantsRunOnStartup) startupManager.Startup = true; + notifyIcon = new NotifyIcon(components); + notifyIcon.Icon = WindowsDarkMode.IsDarkModeEnabled ? Resources.microsoft_xbox_controller_off_white : Resources.microsoft_xbox_controller_off; + notifyIcon.Text = TitleWithVersion; + notifyIcon.Visible = true; + +#if DEBUG + foreach (var profile in Profiles.Dynamic.RoslynDynamicProfile.GetUserProfiles(PreconfiguredUserProfiles)) + { + profile.ErrorsChanged += (errors) => + { + notifyIcon.ShowBalloonTip( + 3000, profile.Name, + String.Join("\n", errors), + ToolTipIcon.Error + ); + }; + profile.Compile(); + profile.Watch(); + context.Profiles.Add(profile); + } +#endif + + // Set available profiles + ProfilesSettings.Helpers.ProfileStringConverter.Profiles = context.Profiles. + Where((profile) => profile.Visible). + Select((profile) => profile.Name).ToArray(); + var contextMenu = new ContextMenuStrip(components); var enabledItem = new ToolStripMenuItem("&Enabled"); @@ -81,7 +109,12 @@ namespace SteamController var profileItem = new ToolStripMenuItem(profile.Name); profileItem.Click += delegate { context.SelectProfile(profile.Name); }; - contextMenu.Opening += delegate { profileItem.Checked = context.CurrentProfile == profile; }; + contextMenu.Opening += delegate + { + profileItem.Checked = context.CurrentProfile == profile; + profileItem.ToolTipText = String.Join("\n", profile.Errors ?? new string[0]); + profileItem.Enabled = profile.Errors is null; + }; contextMenu.Items.Add(profileItem); } @@ -116,10 +149,6 @@ namespace SteamController var exitItem = contextMenu.Items.Add("&Exit"); exitItem.Click += delegate { Application.Exit(); }; - notifyIcon = new NotifyIcon(components); - notifyIcon.Icon = WindowsDarkMode.IsDarkModeEnabled ? Resources.microsoft_xbox_controller_off_white : Resources.microsoft_xbox_controller_off; - notifyIcon.Text = TitleWithVersion; - notifyIcon.Visible = true; notifyIcon.ContextMenuStrip = contextMenu; var contextStateUpdate = new System.Windows.Forms.Timer(components); diff --git a/SteamController/Devices/SteamAction.cs b/SteamController/Devices/SteamAction.cs index 1a8241b..e376fe6 100644 --- a/SteamController/Devices/SteamAction.cs +++ b/SteamController/Devices/SteamAction.cs @@ -149,7 +149,7 @@ namespace SteamController.Devices /// Generated when button was repeated for a given period /// but triggered exactly once - public bool HoldRepeat(TimeSpan duration, TimeSpan repeatEvery, string consume) + public bool HoldRepeat(TimeSpan duration, TimeSpan repeatEvery, string? consume) { // always generate at least one keypress if (Pressed(duration)) diff --git a/SteamController/Devices/SteamController.cs b/SteamController/Devices/SteamController.cs index 2ae2656..fca36c7 100644 --- a/SteamController/Devices/SteamController.cs +++ b/SteamController/Devices/SteamController.cs @@ -14,6 +14,7 @@ namespace SteamController.Devices private Stopwatch stopwatch = new Stopwatch(); private TimeSpan? lastUpdate; private int failures; + public long ElapsedMilliseconds { get => stopwatch.ElapsedMilliseconds; } public double DeltaTime { get; private set; } internal SteamController() diff --git a/SteamController/Profiles/Dynamic/Globals.cs b/SteamController/Profiles/Dynamic/Globals.cs new file mode 100644 index 0000000..358d9cb --- /dev/null +++ b/SteamController/Profiles/Dynamic/Globals.cs @@ -0,0 +1,244 @@ +using Nefarius.ViGEm.Client.Targets.Xbox360; +using SteamController.ProfilesSettings; + +namespace SteamController.Profiles.Dynamic +{ + [Flags] + public enum KeyModifiers + { + MOD_None = 0, + MOD_SHIFT = 1, + MOD_ALT = 2, + MOD_CONTROL = 4, + MOD_WIN = 8 + } + + public class Globals + { + private const string Consumed = "RoslynGlobals"; + + private RoslynDynamicProfile _profile; + private Context _context; + + public class SteamAPI + { + internal Devices.SteamController Target; + internal SteamAPI(Devices.SteamController target) { Target = target; } + + public struct Button + { + internal Devices.SteamButton Target; + public static implicit operator bool(Button button) => button.Target; + public Button(Devices.SteamButton target) { Target = target; } + } + + public struct Axis + { + internal Devices.SteamAxis Target; + public static implicit operator short(Axis button) => button.Target; + public Axis(Devices.SteamAxis target) { Target = target; } + } + + public Button BtnL5 { get => new Button(Target.BtnL5); } + public Button BtnOptions { get => new Button(Target.BtnOptions); } + public Button BtnSteam { get => new Button(Target.BtnSteam); } + public Button BtnMenu { get => new Button(Target.BtnMenu); } + public Button BtnDpadDown { get => new Button(Target.BtnDpadDown); } + public Button BtnDpadLeft { get => new Button(Target.BtnDpadLeft); } + public Button BtnDpadRight { get => new Button(Target.BtnDpadRight); } + public Button BtnDpadUp { get => new Button(Target.BtnDpadUp); } + public Button BtnA { get => new Button(Target.BtnA); } + public Button BtnX { get => new Button(Target.BtnX); } + public Button BtnB { get => new Button(Target.BtnB); } + public Button BtnY { get => new Button(Target.BtnY); } + public Button BtnL1 { get => new Button(Target.BtnL1); } + public Button BtnL2 { get => new Button(Target.BtnL2); } + public Button BtnR1 { get => new Button(Target.BtnR1); } + public Button BtnR2 { get => new Button(Target.BtnR2); } + public Button BtnLeftStickPress { get => new Button(Target.BtnLeftStickPress); } + public Button BtnLPadTouch { get => new Button(Target.BtnLPadTouch); } + public Button BtnLPadPress { get => new Button(Target.BtnLPadPress); } + public Button BtnRPadPress { get => new Button(Target.BtnRPadPress); } + public Button BtnRPadTouch { get => new Button(Target.BtnRPadTouch); } + public Button BtnR5 { get => new Button(Target.BtnR5); } + public Button BtnRightStickPress { get => new Button(Target.BtnRightStickPress); } + public Button BtnLStickTouch { get => new Button(Target.BtnLStickTouch); } + public Button BtnRStickTouch { get => new Button(Target.BtnRStickTouch); } + public Button BtnR4 { get => new Button(Target.BtnR4); } + public Button BtnL4 { get => new Button(Target.BtnL4); } + public Button BtnQuickAccess { get => new Button(Target.BtnQuickAccess); } + + public Button BtnVirtualLeftThumbUp { get => new Button(Target.BtnVirtualLeftThumbUp); } + public Button BtnVirtualLeftThumbDown { get => new Button(Target.BtnVirtualLeftThumbDown); } + public Button BtnVirtualLeftThumbLeft { get => new Button(Target.BtnVirtualLeftThumbLeft); } + public Button BtnVirtualLeftThumbRight { get => new Button(Target.BtnVirtualLeftThumbRight); } + + public Axis LPadX { get => new Axis(Target.LPadX); } + public Axis LPadY { get => new Axis(Target.LPadY); } + public Axis RPadX { get => new Axis(Target.RPadX); } + public Axis RPadY { get => new Axis(Target.RPadY); } + public Axis AccelX { get => new Axis(Target.AccelX); } + public Axis AccelY { get => new Axis(Target.AccelY); } + public Axis AccelZ { get => new Axis(Target.AccelZ); } + public Axis GyroPitch { get => new Axis(Target.GyroPitch); } + public Axis GyroYaw { get => new Axis(Target.GyroYaw); } + public Axis GyroRoll { get => new Axis(Target.GyroRoll); } + public Axis LeftTrigger { get => new Axis(Target.LeftTrigger); } + public Axis RightTrigger { get => new Axis(Target.RightTrigger); } + public Axis LeftThumbX { get => new Axis(Target.LeftThumbX); } + public Axis LeftThumbY { get => new Axis(Target.LeftThumbY); } + public Axis RightThumbX { get => new Axis(Target.RightThumbX); } + public Axis RightThumbY { get => new Axis(Target.RightThumbY); } + public Axis LPadPressure { get => new Axis(Target.LPadPressure); } + public Axis RPadPressure { get => new Axis(Target.RPadPressure); } + } + + public class KeyboardAPI + { + internal Devices.KeyboardController Target; + internal KeyboardAPI(Devices.KeyboardController target) { Target = target; } + + public bool this[VirtualKeyCode key] + { + get { return Target[key.ToWindowsInput()]; } + set { Target.Overwrite(key.ToWindowsInput(), value); } + } + + public void KeyPress(params VirtualKeyCode[] keyCodes) + { + KeyPress(KeyModifiers.MOD_None, keyCodes); + } + + public void KeyPress(KeyModifiers modifiers, params VirtualKeyCode[] keyCodes) + { + var virtualCodes = keyCodes.Select((code) => (WindowsInput.VirtualKeyCode)code).ToArray(); + + if (modifiers != KeyModifiers.MOD_None) + { + List modifierCodes = new List(); + if (modifiers.HasFlag(KeyModifiers.MOD_SHIFT)) + modifierCodes.Add(WindowsInput.VirtualKeyCode.SHIFT); + if (modifiers.HasFlag(KeyModifiers.MOD_CONTROL)) + modifierCodes.Add(WindowsInput.VirtualKeyCode.CONTROL); + if (modifiers.HasFlag(KeyModifiers.MOD_ALT)) + modifierCodes.Add(WindowsInput.VirtualKeyCode.MENU); + if (modifiers.HasFlag(KeyModifiers.MOD_WIN)) + modifierCodes.Add(WindowsInput.VirtualKeyCode.LWIN); + Target.KeyPress(modifierCodes, virtualCodes); + } + else + { + Target.KeyPress(virtualCodes); + } + } + } + + public class MouseAPI + { + internal Devices.MouseController Target; + internal MouseAPI(Devices.MouseController target) { Target = target; } + + public struct Button + { + internal Devices.MouseController? Controller = null; + internal Devices.MouseController.Button Target = Devices.MouseController.Button.Left; + internal bool Value; + + internal Button(Devices.MouseController controller, Devices.MouseController.Button target) { Controller = controller; Target = target; Value = Controller[Target]; } + internal Button(bool value) { this.Value = value; } + + public void Click() { Controller?.MouseClick(Target); } + public void DoubleClick() { Controller?.MouseClick(Target); } + + internal void Set(Button value) { Controller?.Overwrite(Target, value.Value); } + + public static implicit operator Button(bool value) { return new Button(value); } + } + + public Button BtnLeft { get => new Button(Target, Devices.MouseController.Button.Left); set => this.BtnLeft.Set(value); } + public Button BtnRight { get => new Button(Target, Devices.MouseController.Button.Right); set => this.BtnRight.Set(value); } + public Button BtnMiddle { get => new Button(Target, Devices.MouseController.Button.Middle); set => this.BtnMiddle.Set(value); } + public Button BtnX { get => new Button(Target, Devices.MouseController.Button.X); set => this.BtnX.Set(value); } + public Button BtnY { get => new Button(Target, Devices.MouseController.Button.Y); set => this.BtnY.Set(value); } + + public void MoveBy(double pixelDeltaX, double pixelDeltaY) { Target.MoveBy(pixelDeltaX, pixelDeltaY); } + public void MoveTo(double absoluteX, double absoluteY) { Target.MoveTo(absoluteX, absoluteY); } + public void VerticalScroll(double scrollAmountInClicks) { Target.VerticalScroll(scrollAmountInClicks); } + public void HorizontalScroll(double scrollAmountInClicks) { Target.HorizontalScroll(scrollAmountInClicks); } + } + + public class X360API + { + internal const int MinimumPresTimeMilliseconds = 30; + internal Devices.Xbox360Controller Target; + internal X360API(Devices.Xbox360Controller target) { Target = target; } + + public bool Connected { set => Target.Connected = value; } + + public bool BtnUp { set => Target.Overwrite(Xbox360Button.Up, value, MinimumPresTimeMilliseconds); } + public bool BtnDown { set => Target.Overwrite(Xbox360Button.Down, value, MinimumPresTimeMilliseconds); } + public bool BtnLeft { set => Target.Overwrite(Xbox360Button.Left, value, MinimumPresTimeMilliseconds); } + public bool BtnRight { set => Target.Overwrite(Xbox360Button.Right, value, MinimumPresTimeMilliseconds); } + public bool BtnA { set => Target.Overwrite(Xbox360Button.A, value, MinimumPresTimeMilliseconds); } + public bool BtnB { set => Target.Overwrite(Xbox360Button.B, value, MinimumPresTimeMilliseconds); } + public bool BtnX { set => Target.Overwrite(Xbox360Button.X, value, MinimumPresTimeMilliseconds); } + public bool BtnY { set => Target.Overwrite(Xbox360Button.Y, value, MinimumPresTimeMilliseconds); } + public bool BtnGuide { set => Target.Overwrite(Xbox360Button.Guide, value, MinimumPresTimeMilliseconds); } + public bool BtnBack { set => Target.Overwrite(Xbox360Button.Back, value, MinimumPresTimeMilliseconds); } + public bool BtnStart { set => Target.Overwrite(Xbox360Button.Start, value, MinimumPresTimeMilliseconds); } + public bool BtnLeftShoulder { set => Target.Overwrite(Xbox360Button.LeftShoulder, value, MinimumPresTimeMilliseconds); } + public bool BtnRightShoulder { set => Target.Overwrite(Xbox360Button.RightShoulder, value, MinimumPresTimeMilliseconds); } + public bool BtnLeftThumb { set => Target.Overwrite(Xbox360Button.LeftThumb, value, MinimumPresTimeMilliseconds); } + public bool BtnRightThumb { set => Target.Overwrite(Xbox360Button.RightThumb, value, MinimumPresTimeMilliseconds); } + public short AxisLeftThumbX { set => Target[Xbox360Axis.LeftThumbX] = value; } + public short AxisLeftThumbY { set => Target[Xbox360Axis.LeftThumbY] = value; } + public short AxisRightThumbX { set => Target[Xbox360Axis.RightThumbX] = value; } + public short AxisRightThumbY { set => Target[Xbox360Axis.RightThumbY] = value; } + public short SliderLeftTrigger { set => Target[Xbox360Slider.LeftTrigger] = value; } + public short SliderRightTrigger { set => Target[Xbox360Slider.RightTrigger] = value; } + } + + private SteamAPI? _steamAPI; + private X360API? _x360API; + private KeyboardAPI? _keyboardAPI; + private MouseAPI? _mouseAPI; + + public SteamAPI Steam { get => _steamAPI ??= new SteamAPI(_context.Steam); } + public X360API X360 { get => _x360API ??= new X360API(_context.X360); } + public KeyboardAPI Keyboard { get => _keyboardAPI ??= new KeyboardAPI(_context.Keyboard); } + public MouseAPI Mouse { get => _mouseAPI ??= new MouseAPI(_context.Mouse); } + + public Globals(RoslynDynamicProfile profile, Context context) + { + this._profile = profile; + this._context = context; + } + + public bool Pressed(SteamAPI.Button button) + { + return button.Target.Pressed(); + } + + public bool JustPressed(SteamAPI.Button button) + { + return button.Target.JustPressed(); + } + + public bool HoldFor(SteamAPI.Button button, int minTimeMs) + { + return button.Target.Hold(TimeSpan.FromMilliseconds(minTimeMs), null); + } + + public bool Turbo(SteamAPI.Button button, int timesPerSec) + { + var interval = TimeSpan.FromMilliseconds(1000 / timesPerSec); + return button.Target.JustPressed() || button.Target.HoldRepeat(interval, interval, null); + } + + public void Log(string format, params object?[] arg) + { + var output = String.Format(format, arg); + CommonHelpers.Log.TraceLine("{0}: {1}: {2}", _profile.Name, _context.Steam.ElapsedMilliseconds, output); + } + } +} diff --git a/SteamController/Profiles/Dynamic/RoslynDynamicProfile.cs b/SteamController/Profiles/Dynamic/RoslynDynamicProfile.cs new file mode 100644 index 0000000..ca20500 --- /dev/null +++ b/SteamController/Profiles/Dynamic/RoslynDynamicProfile.cs @@ -0,0 +1,186 @@ +using System.Reflection; +using CommonHelpers; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; + +namespace SteamController.Profiles.Dynamic +{ + public sealed partial class RoslynDynamicProfile : Profile + { + private const int ScriptTimeout = 10; // max 10ms + + private String fileName; + private Script? compiledScript; + private Profile? inherited; + private DateTime? lastModifiedTime; + private System.Windows.Forms.Timer? watchTimer; + + public RoslynDynamicProfile(string name, string fileName, Profile? inherited = null) + { + this.fileName = fileName; + this.inherited = inherited; + this.Name = name; + if (inherited is not null) + this.Name = inherited.Name + ": " + name; + this.Visible = inherited?.Visible ?? true; + this.IsDesktop = inherited?.IsDesktop ?? false; + } + + private static ScriptOptions CompilationOptions + { + get + { + var options = ScriptOptions.Default; + + // Add Keyboard controls + options = options.AddReferences(typeof(KeyModifiers).Assembly); + options = options.AddImports(typeof(KeyModifiers).FullName); + options = options.AddReferences(typeof(ProfilesSettings.VirtualKeyCode).Assembly); + options = options.AddImports(typeof(ProfilesSettings.VirtualKeyCode).FullName); + return options; + } + } + + public bool Compile() + { + this.compiledScript = null; + this.lastModifiedTime = null; + this.Errors = null; + + if (!File.Exists(fileName)) + { + this.Errors = new string[] { String.Format("File '{0}' does not exist.", fileName) }; + return false; + } + + try + { + this.lastModifiedTime = File.GetLastWriteTimeUtc(fileName); + + using (var file = System.IO.File.OpenRead(fileName)) + { + var options = CompilationOptions.WithFilePath(Path.GetFileName(fileName)); + var script = CSharpScript.Create(file, options, typeof(Globals)); + var compileResult = script.Compile(); + var errors = compileResult.Where((result) => result.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error); + + if (!errors.Any()) + { + this.compiledScript = script; + return true; + } + + this.Errors = errors.Select((result) => result.ToString()).ToArray(); + OnErrorsChanged(); + } + } + catch (Exception e) + { + this.Errors = new string[] { e.Message }; + OnErrorsChanged(); + } + + Log.TraceLine("UserProfile: {0}: Compilation Error", fileName); + foreach (var error in this.Errors) + Log.TraceLine("\t{0}", error); + return false; + } + + public void Watch() + { + if (this.lastModifiedTime is null) + return; + if (this.watchTimer is not null) + return; + + watchTimer = new System.Windows.Forms.Timer(); + watchTimer.Interval = 1000; + watchTimer.Tick += delegate + { + try + { + if (this.lastModifiedTime is null) + return; + + var latest = File.GetLastWriteTimeUtc(fileName); + if (this.lastModifiedTime >= latest) + return; + + Log.TraceLine("UserProfile: {0}. Detected modification: '{1}' vs '{2}'", fileName, this.lastModifiedTime, latest); + } + catch (Exception) { return; } + + Compile(); + }; + watchTimer.Start(); + } + + public override bool Selected(Context context) + { + return (this.compiledScript is not null) && (inherited?.Selected(context) ?? true); + } + + public override Status Run(Context context) + { + if (inherited?.Run(context).IsDone ?? false) + return Status.Done; + + if (this.compiledScript is null) + return Status.Continue; + + try + { + var cancelToken = new CancellationTokenSource(); + var task = this.compiledScript.RunAsync(new Globals(this, context), cancelToken.Token); + if (!task.Wait(ScriptTimeout)) + { + cancelToken.Cancel(); + task.Wait(); + Log.TraceLine("UserProfile: {0}: Timedout. Canceled."); + } + } + catch (Exception e) + { + Log.TraceLine("UserProfile: {0}: {1}", fileName, e); + } + + return Status.Continue; + } + + public static IEnumerable GetUserProfiles(Dictionary preconfiguredProfiles) + { + var files = new Dictionary(); + + foreach (var directory in GetUserProfilesPaths()) + { + foreach (var profile in preconfiguredProfiles) + { + foreach (string file in Directory.GetFiles(directory, profile.Key)) + { + String name = Path.GetFileNameWithoutExtension(file); + name = Path.GetFileNameWithoutExtension(name); + + yield return new RoslynDynamicProfile(name, file, profile.Value); + } + } + } + } + + private static IEnumerable GetUserProfilesPaths() + { + var exePath = Assembly.GetExecutingAssembly().Location; + var exeFolder = Path.GetDirectoryName(exePath); + if (exeFolder is not null) + { + var exeUserProfiles = Path.Combine(exeFolder, "UserProfiles"); + if (Directory.Exists(exeUserProfiles)) + yield return exeUserProfiles; + } + + var documentsFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + var steamControllerDocumentsFolder = Path.Combine(documentsFolder, "SteamController", "UserProfiles"); + if (Directory.Exists(steamControllerDocumentsFolder)) + yield return steamControllerDocumentsFolder; + } + } +} diff --git a/SteamController/Profiles/Profile.cs b/SteamController/Profiles/Profile.cs index b975bfd..86b5861 100644 --- a/SteamController/Profiles/Profile.cs +++ b/SteamController/Profiles/Profile.cs @@ -10,12 +10,25 @@ namespace SteamController.Profiles public bool IsDone { get; set; } } + public event Action ErrorsChanged; + public virtual String Name { get; set; } = ""; public virtual bool Visible { get; set; } = true; public virtual bool IsDesktop { get; set; } + public virtual string[]? Errors { get; set; } public abstract bool Selected(Context context); public abstract Status Run(Context context); + + public Profile() + { + ErrorsChanged += delegate { }; + } + + protected void OnErrorsChanged() + { + ErrorsChanged(this.Errors ?? new string[0]); + } } } diff --git a/SteamController/SteamController.csproj b/SteamController/SteamController.csproj index 16f36d2..963ad89 100644 --- a/SteamController/SteamController.csproj +++ b/SteamController/SteamController.csproj @@ -10,6 +10,10 @@ Resources\microsoft-xbox-controller.ico + + + + @@ -18,8 +22,15 @@ + + + PreserveNewest + + + + diff --git a/SteamController/UserProfiles/Turbo.x360.cs b/SteamController/UserProfiles/Turbo.x360.cs new file mode 100644 index 0000000..d5d8e2d --- /dev/null +++ b/SteamController/UserProfiles/Turbo.x360.cs @@ -0,0 +1,9 @@ +// If L5 is hold, the A, B, X, Y is turbo: 10x per second +if (Steam.BtnL5) +{ + X360.BtnA = Turbo(Steam.BtnA, 10); + X360.BtnB = Turbo(Steam.BtnB, 10); + X360.BtnX = Turbo(Steam.BtnX, 10); + X360.BtnY = Turbo(Steam.BtnY, 10); +} +