ScpControl.Shared/Core/DualShockProfile.cs (367 lines of code) (raw):

using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Runtime.Serialization; using WindowsInput; using WindowsInput.Native; using PropertyChanged; namespace ScpControl.Shared.Core { /// <summary> /// Possible mapping target types (keystrokes, mouse movement etc.) /// </summary> public enum CommandType : byte { [Description("Keystrokes")] Keystrokes, [Description("Gamepad buttons")] GamepadButton, [Description("Mouse buttons")] MouseButtons, [Description("Mouse axis")] MouseAxis } /// <summary> /// Describes a mapping target. /// </summary> [ImplementPropertyChanged] [DataContract] [KnownType(typeof (DsButtonMappingTarget))] public class DsButtonMappingTarget { #region Properties [DataMember] public CommandType CommandType { get; set; } [DataMember] public object CommandTarget { get; set; } #endregion } /// <summary> /// Represents a DualShock button/axis mapping profile. /// </summary> [ImplementPropertyChanged] [DataContract] [KnownType(typeof (Ds3Button))] [KnownType(typeof (Ds4Button))] [KnownType(typeof (VirtualKeyCode))] [KnownType(typeof (MouseButton))] [DisplayName("DualShock Profile")] public class DualShockProfile { #region Ctor public DualShockProfile() { Id = Guid.NewGuid(); Name = "New Profile"; OnCreated(); } [OnDeserializing] private void OnDeserializing(StreamingContext c) { OnCreated(); } /// <summary> /// Initialize buttons/axes. /// </summary> private void OnCreated() { Ps = new DsButtonProfile(Ds3Button.Ps, Ds4Button.Ps); Circle = new DsButtonProfile(Ds3Button.Circle, Ds4Button.Circle); Cross = new DsButtonProfile(Ds3Button.Cross, Ds4Button.Cross); Square = new DsButtonProfile(Ds3Button.Square, Ds4Button.Square); Triangle = new DsButtonProfile(Ds3Button.Triangle, Ds4Button.Triangle); Select = new DsButtonProfile(Ds3Button.Select, Ds4Button.Share); Start = new DsButtonProfile(Ds3Button.Start, Ds4Button.Options); LeftShoulder = new DsButtonProfile(Ds3Button.L1, Ds4Button.L1); RightShoulder = new DsButtonProfile(Ds3Button.R1, Ds4Button.R1); LeftTrigger = new DsButtonProfile(Ds3Button.L2, Ds4Button.L2); RightTrigger = new DsButtonProfile(Ds3Button.R2, Ds4Button.R2); LeftThumb = new DsButtonProfile(Ds3Button.L3, Ds4Button.L3); RightThumb = new DsButtonProfile(Ds3Button.R3, Ds4Button.R3); // D-Pad Up = new DsButtonProfile(Ds3Button.Up, Ds4Button.Up); Right = new DsButtonProfile(Ds3Button.Right, Ds4Button.Right); Down = new DsButtonProfile(Ds3Button.Down, Ds4Button.Down); Left = new DsButtonProfile(Ds3Button.Left, Ds4Button.Left); } #endregion #region Public methods /// <summary> /// Applies button re-mapping to the supplied report. /// </summary> /// <param name="report">The report to manipulate.</param> public void Remap(ScpHidReport report) { // determine if profile should be applied switch (Match) { case DsMatch.Global: // always apply break; case DsMatch.Mac: // applies of MAC address matches var reportMac = report.PadMacAddress.ToString(); if (string.CompareOrdinal(MacAddress.Replace(":", string.Empty), reportMac) != 0) return; break; case DsMatch.None: // never apply return; case DsMatch.Pad: // applies if pad IDs match if (PadId != report.PadId) return; break; } // walk through all buttons foreach (var buttonProfile in Buttons) { buttonProfile.Remap(report); } } public override bool Equals(object obj) { var profile = obj as DualShockProfile; return profile != null && profile.Name.Equals(Name); } public override int GetHashCode() { return Id.GetHashCode() ^ Name.GetHashCode(); } public override string ToString() { return Name; } #endregion #region Properties [DataMember] [Category("Main")] [DisplayName("Profile is active")] [Description("If disabled, the entire profile will be ignored from processing.")] public bool IsActive { get; set; } [DataMember] [Category("Main")] [DisplayName("Profile name")] [Description("The friendly name of this profile.")] public string Name { get; set; } [DataMember] [ReadOnly(true)] [Description("The unique identifier of this profile.")] public Guid Id { get; private set; } [DataMember] [ReadOnly(true)] [DisplayName("Pad ID")] public DsPadId PadId { get; set; } [DataMember] [ReadOnly(true)] [DisplayName("MAC Address")] public string MacAddress { get; set; } [DataMember] [ReadOnly(true)] [DisplayName("Pad Model")] public DsModel Model { get; set; } [DataMember] [Category("Main")] [DisplayName("Match profile on")] public DsMatch Match { get; set; } [Browsable(false)] private IEnumerable<DsButtonProfile> Buttons { get { var props = GetType().GetProperties().Where(pi => pi.PropertyType == typeof (DsButtonProfile)); return props.Select(b => b.GetValue(this)).Cast<DsButtonProfile>(); } } [DataMember] [Browsable(false)] public DsButtonProfile Ps { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile Circle { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile Cross { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile Square { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile Triangle { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile Select { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile Start { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile LeftShoulder { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile RightShoulder { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile LeftTrigger { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile RightTrigger { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile LeftThumb { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile RightThumb { get; set; } // D-Pad [DataMember] [Browsable(false)] public DsButtonProfile Up { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile Right { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile Down { get; set; } [DataMember] [Browsable(false)] public DsButtonProfile Left { get; set; } #endregion } /// <summary> /// Describes details about individual buttons. /// </summary> [ImplementPropertyChanged] [DataContract] public class DsButtonProfile { private static readonly InputSimulator VirtualInput = new InputSimulator(); private const uint InputDelay = 100; #region Ctor public DsButtonProfile() { OnCreated(); } /// <summary> /// Creates a new button mapping profile. /// </summary> /// <param name="sources">A list of DualShock buttons which will be affected by this profile.</param> public DsButtonProfile(params IDsButton[] sources) : this() { SourceButtons = sources; } #endregion #region Public methods /// <summary> /// Applies button re-mapping to the supplied report. /// </summary> /// <param name="report">The report to manipulate.</param> public void Remap(ScpHidReport report) { // skip disabled mapping if (!IsEnabled) return; switch (MappingTarget.CommandType) { case CommandType.GamepadButton: foreach (var button in SourceButtons) { // turbo is special, apply first if (Turbo.IsEnabled) { Turbo.ApplyOn(report, button); } // get target button IDsButton target = MappingTarget.CommandTarget as Ds3Button; // if target is no valid button or none, skip setting it if (target == null) continue; // if it's a DS4, translate button if (report.Model == DsModel.DS4) { target = Ds4Button.Buttons.First(b => b.Name.Equals(target.Name)); } // if original isn't pressed we can ignore if (!report[button].IsPressed) continue; // unset original button report.Unset(button); // set new button report.Set(target); } break; case CommandType.Keystrokes: foreach (var button in SourceButtons) { var target = (VirtualKeyCode) Enum.ToObject(typeof(VirtualKeyCode), MappingTarget.CommandTarget); if (report[button].IsPressed) { VirtualInput.Keyboard.KeyDown(target); } else { VirtualInput.Keyboard.KeyUp(target); } } break; } } #endregion #region Properties [DataMember] private IEnumerable<IDsButton> SourceButtons { get; set; } [DataMember] public DsButtonMappingTarget MappingTarget { get; private set; } [DataMember] public bool IsEnabled { get; set; } [DataMember] public DsButtonProfileTurboSetting Turbo { get; set; } public byte CurrentValue { get; set; } #endregion #region Deserialization private void OnCreated() { MappingTarget = new DsButtonMappingTarget(); Turbo = new DsButtonProfileTurboSetting(); } [OnDeserializing] private void OnDeserializing(StreamingContext c) { OnCreated(); } #endregion } /// <summary> /// Describes button turbo mode details. /// </summary> [ImplementPropertyChanged] [DataContract] public class DsButtonProfileTurboSetting { #region Ctor public DsButtonProfileTurboSetting() { Delay = 0; Interval = 50; Release = 100; } #endregion #region Public methods /// <summary> /// Applies turbo algorithm for a specified <see cref="IDsButton" /> on a given <see cref="ScpHidReport" />. /// </summary> /// <param name="report">The HID report to manipulate.</param> /// <param name="button">The button to trigger turbo on.</param> public void ApplyOn(ScpHidReport report, IDsButton button) { // button type must match model, madness otherwise! if ((report.Model != DsModel.DS3 || !(button is Ds3Button)) && (report.Model != DsModel.DS4 || !(button is Ds4Button))) return; // if button got released... if (_isActive && !report[button].IsPressed) { // ...disable, reset and return _isActive = false; _delayedFrame.Reset(); _engagedFrame.Reset(); _releasedFrame.Reset(); return; } // if turbo is enabled and button is pressed... if (!_isActive && report[button].IsPressed) { // ...start calculating the activation delay... if (!_delayedFrame.IsRunning) _delayedFrame.Restart(); // ...if we are still activating, don't do anything if (_delayedFrame.ElapsedMilliseconds < Delay) return; // time to activate! _isActive = true; _delayedFrame.Reset(); } // if the button was released... if (!report[button].IsPressed) { // ...restore default states and skip processing _isActive = false; return; } // reset engaged ("keep pressed") time frame... if (!_engagedFrame.IsRunning) _engagedFrame.Restart(); // ...do not change state while within frame and button is still pressed, then skip if (_engagedFrame.ElapsedMilliseconds < Interval && report[button].IsPressed) return; // reset released time frame ("forecefully release") for button if (!_releasedFrame.IsRunning) _releasedFrame.Restart(); // while we're still within the released time frame... if (_releasedFrame.ElapsedMilliseconds < Release) { // ...re-set the button state to released report.Unset(button); } else { // all frames passed, reset and start over _isActive = false; _delayedFrame.Stop(); _engagedFrame.Stop(); _releasedFrame.Stop(); } } #endregion #region Private fields private Stopwatch _delayedFrame = new Stopwatch(); private Stopwatch _engagedFrame = new Stopwatch(); private bool _isActive; private Stopwatch _releasedFrame = new Stopwatch(); #endregion #region Properties /// <summary> /// True if turbo mode is enabled for the current button, false otherwise. /// </summary> [DataMember] public bool IsEnabled { get; set; } /// <summary> /// The delay (in milliseconds) afther which the turbo mode shall engage (default is immediate). /// </summary> [DataMember] public int Delay { get; set; } /// <summary> /// The timespan (in milliseconds) the button should be reported as remaining pressed to the output. /// </summary> [DataMember] public int Interval { get; set; } /// <summary> /// The timespan (in milliseconds) the button state should be reported as released so the turbo event can repeat again. /// </summary> [DataMember] public int Release { get; set; } #endregion #region Deserialization private void OnCreated() { _delayedFrame = new Stopwatch(); _engagedFrame = new Stopwatch(); _releasedFrame = new Stopwatch(); _isActive = false; } [OnDeserializing] private void OnDeserializing(StreamingContext c) { OnCreated(); } #endregion } }