Allow configuring "keybinds" for haptic feedback (see desc.)

Open `Config` > `Controllers...` with any rom loaded and go to the last tab.
A "Debug" virtual channel is hardcoded, and will be present on every core.
As with the previous commit, holding Fast Forward causes this channel to fire
and it will be passed through to a bound host gamepad.
The prescale slider works. Virtual channels with a player number prefix also
work, but the single "Debug" channel without a prefix is hardcoded.
Caveats: Reopening the config dialog doesn't load the host channel back into the
combobox. It will save to config correctly.
This commit is contained in:
YoshiRulz 2021-03-29 11:21:02 +10:00
parent 6102db0e68
commit bb3fddcb5f
No known key found for this signature in database
GPG Key ID: C4DE31C245353FB7
11 changed files with 282 additions and 17 deletions

View File

@ -30,6 +30,7 @@ namespace BizHawk.Client.Common
private readonly WorkingDictionary<string, int> _axes = new WorkingDictionary<string, int>();
private readonly Dictionary<string, AxisSpec> _axisRanges = new WorkingDictionary<string, AxisSpec>();
private readonly Dictionary<string, AnalogBind> _axisBindings = new Dictionary<string, AnalogBind>();
private readonly Dictionary<string, FeedbackBind> _feedbackBindings = new Dictionary<string, FeedbackBind>();
/// <summary>don't do this</summary>
public void ForceType(ControllerDefinition newType) => Definition = newType;
@ -94,6 +95,14 @@ namespace BizHawk.Client.Common
}
}
public void PrepareHapticsForHost(SimpleController finalHostController, int debug)
{
foreach (var kvp in _feedbackBindings)
{
finalHostController.SetHapticChannelStrength(kvp.Value.GamepadPrefix + kvp.Value.Channel, (int) ((double) debug * kvp.Value.Prescale));
}
}
public void ApplyAxisConstraints(string constraintClass)
=> Definition.ApplyAxisConstraints(constraintClass, _axes);
@ -153,6 +162,8 @@ namespace BizHawk.Client.Common
_axisBindings[button] = bind;
}
public void BindFeedbackChannel(string channel, FeedbackBind binding) => _feedbackBindings[channel] = binding;
public List<string> PressedButtons => _buttons
.Where(kvp => kvp.Value)
.Select(kvp => kvp.Key)

View File

@ -22,12 +22,16 @@ namespace BizHawk.Client.Common
public Config()
{
if (AllTrollers.Count == 0 && AllTrollersAutoFire.Count == 0 && AllTrollersAnalog.Count == 0)
if (AllTrollers.Count == 0
&& AllTrollersAutoFire.Count == 0
&& AllTrollersAnalog.Count == 0
&& AllTrollersFeedbacks.Count == 0)
{
var cd = ConfigService.Load<DefaultControls>(ControlDefaultPath);
AllTrollers = cd.AllTrollers;
AllTrollersAutoFire = cd.AllTrollersAutoFire;
AllTrollersAnalog = cd.AllTrollersAnalog;
AllTrollersFeedbacks = cd.AllTrollersFeedbacks;
}
}
@ -293,6 +297,7 @@ namespace BizHawk.Client.Common
public Dictionary<string, Dictionary<string, string>> AllTrollers { get; set; } = new Dictionary<string, Dictionary<string, string>>();
public Dictionary<string, Dictionary<string, string>> AllTrollersAutoFire { get; set; } = new Dictionary<string, Dictionary<string, string>>();
public Dictionary<string, Dictionary<string, AnalogBind>> AllTrollersAnalog { get; set; } = new Dictionary<string, Dictionary<string, AnalogBind>>();
public Dictionary<string, Dictionary<string, FeedbackBind>> AllTrollersFeedbacks { get; set; } = new Dictionary<string, Dictionary<string, FeedbackBind>>();
/// <remarks>as this setting spans multiple cores and doesn't actually affect the behavior of any core, it hasn't been absorbed into the new system</remarks>
public bool GbAsSgb { get; set; }

View File

@ -13,5 +13,8 @@ namespace BizHawk.Client.Common
public Dictionary<string, Dictionary<string, AnalogBind>> AllTrollersAnalog { get; set; }
= new Dictionary<string, Dictionary<string, AnalogBind>>();
public Dictionary<string, Dictionary<string, FeedbackBind>> AllTrollersFeedbacks { get; set; }
= new Dictionary<string, Dictionary<string, FeedbackBind>>();
}
}

View File

@ -0,0 +1,26 @@
#nullable enable
using Newtonsoft.Json;
namespace BizHawk.Client.Common
{
public struct FeedbackBind
{
public string? Channel;
/// <remarks>"X# "/"J# " (with the trailing space)</remarks>
public string? GamepadPrefix;
[JsonIgnore]
public bool IsZeroed => GamepadPrefix == null;
public float Prescale;
public FeedbackBind(string prefix, string channel, float prescale)
{
GamepadPrefix = prefix;
Channel = channel;
Prescale = prescale;
}
}
}

View File

@ -53,7 +53,7 @@ namespace BizHawk.Client.Common
{
var def = emulator.ControllerDefinition;
ActiveController = BindToDefinition(def, config.AllTrollers, config.AllTrollersAnalog);
ActiveController = BindToDefinition(def, config.AllTrollers, config.AllTrollersAnalog, config.AllTrollersFeedbacks);
AutoFireController = BindToDefinitionAF(emulator, config.AllTrollersAutoFire, config.AutofireOn, config.AutofireOff);
// allow propagating controls that are in the current controller definition but not in the prebaked one
@ -86,7 +86,11 @@ namespace BizHawk.Client.Common
AutofireStickyXorAdapter.MassToggleStickyState(ActiveController.PressedButtons);
}
private static Controller BindToDefinition(ControllerDefinition def, IDictionary<string, Dictionary<string, string>> allBinds, IDictionary<string, Dictionary<string, AnalogBind>> analogBinds)
private static Controller BindToDefinition(
ControllerDefinition def,
IDictionary<string, Dictionary<string, string>> allBinds,
IDictionary<string, Dictionary<string, AnalogBind>> analogBinds,
IDictionary<string, Dictionary<string, FeedbackBind>> feedbackBinds)
{
var ret = new Controller(def);
if (allBinds.TryGetValue(def.Name, out var binds))
@ -111,6 +115,15 @@ namespace BizHawk.Client.Common
}
}
if (feedbackBinds.TryGetValue(def.Name, out var fBinds))
{
const string channel = "Debug";
// foreach (var channel in def.HapticsChannels)
{
if (fBinds.TryGetValue(channel, out var bind)) ret.BindFeedbackChannel(channel, bind);
}
}
return ret;
}

View File

@ -668,14 +668,6 @@ namespace BizHawk.Client.EmuHawk
public override bool BlocksInputWhenFocused { get; } = false;
private static readonly IReadOnlyCollection<string> DEBUG_HAPTIC_CHANNELS = new[]
{
"J0 Mono", "J0 Left", "J0 Right",
"X0 Mono", "X0 Left", "X0 Right",
"J1 Mono", "J1 Left", "J1 Right",
"X1 Mono", "X1 Left", "X1 Right"
};
public int ProgramRunLoop()
{
// needs to be done late, after the log console snaps on top
@ -711,8 +703,9 @@ namespace BizHawk.Client.EmuHawk
// ...but prepare haptics first, those get read in ProcessInput
var finalHostController = (ControllerInputCoalescer) InputManager.ControllerInputCoalescer;
// for now, vibrate the first gamepad when the Fast Forward hotkey is held, using the value from the previous (host) frame
var debugVibrating = InputManager.ClientControls.IsPressed("Fast Forward") ? int.MaxValue : 0;
foreach (var channel in DEBUG_HAPTIC_CHANNELS) finalHostController.SetHapticChannelStrength(channel, debugVibrating);
InputManager.ActiveController.PrepareHapticsForHost(
finalHostController,
debug: InputManager.ClientControls.IsPressed("Fast Forward") ? int.MaxValue : 0);
ProcessInput(
_hotkeyCoalescer,
finalHostController,

View File

@ -33,6 +33,7 @@
this.NormalControlsTab = new System.Windows.Forms.TabPage();
this.AutofireControlsTab = new System.Windows.Forms.TabPage();
this.AnalogControlsTab = new System.Windows.Forms.TabPage();
this.FeedbacksTab = new System.Windows.Forms.TabPage();
this.checkBoxAutoTab = new System.Windows.Forms.CheckBox();
this.buttonOK = new System.Windows.Forms.Button();
this.buttonCancel = new System.Windows.Forms.Button();
@ -59,6 +60,7 @@
this.tabControl1.Controls.Add(this.NormalControlsTab);
this.tabControl1.Controls.Add(this.AutofireControlsTab);
this.tabControl1.Controls.Add(this.AnalogControlsTab);
this.tabControl1.Controls.Add(this.FeedbacksTab);
this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tabControl1.Location = new System.Drawing.Point(3, 3);
this.tabControl1.Name = "tabControl1";
@ -95,6 +97,15 @@
this.AnalogControlsTab.Text = "Analog Controls";
this.AnalogControlsTab.UseVisualStyleBackColor = true;
//
// FeedbacksTab
//
this.FeedbacksTab.Location = new System.Drawing.Point(4, 22);
this.FeedbacksTab.Name = "FeedbacksTab";
this.FeedbacksTab.Size = new System.Drawing.Size(554, 495);
this.FeedbacksTab.TabIndex = 3;
this.FeedbacksTab.Text = "Feedbacks";
this.FeedbacksTab.UseVisualStyleBackColor = true;
//
// checkBoxAutoTab
//
this.checkBoxAutoTab.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
@ -267,6 +278,7 @@
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
private System.Windows.Forms.PictureBox pictureBox1;
private System.Windows.Forms.TabPage AnalogControlsTab;
private System.Windows.Forms.TabPage FeedbacksTab;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
private System.Windows.Forms.ToolTip toolTip1;
private MenuButton btnMisc;

View File

@ -90,6 +90,11 @@ namespace BizHawk.Client.EmuHawk
return new AnalogBindPanel(settings, buttons) { Dock = DockStyle.Fill, AutoScroll = true };
}
private static Control CreateFeedbacksPanel(Dictionary<string, FeedbackBind> settings, List<string> buttons, Size size)
{
return new FeedbacksBindPanel(settings, buttons) { Dock = DockStyle.Fill, AutoScroll = true };
}
private static readonly Regex ButtonMatchesPlayer = new Regex("^P(\\d+)\\s");
private void LoadToPanel<TBindValue>(
@ -201,7 +206,8 @@ namespace BizHawk.Client.EmuHawk
private void LoadPanels(
IDictionary<string, Dictionary<string, string>> normal,
IDictionary<string, Dictionary<string, string>> autofire,
IDictionary<string, Dictionary<string, AnalogBind>> analog)
IDictionary<string, Dictionary<string, AnalogBind>> analog,
IDictionary<string, Dictionary<string, FeedbackBind>> haptics)
{
LoadToPanel(
NormalControlsTab,
@ -230,21 +236,30 @@ namespace BizHawk.Client.EmuHawk
new AnalogBind("", 1.0f, 0.1f),
CreateAnalogPanel
);
LoadToPanel(
FeedbacksTab,
_emulator.ControllerDefinition.Name,
/*_emulator.ControllerDefinition.HapticsChannels*/new[] { "Debug" },
_emulator.ControllerDefinition.CategoryLabels,
haptics,
new(string.Empty, string.Empty, 1.0f),
CreateFeedbacksPanel);
if (AnalogControlsTab.Controls.Count == 0)
{
tabControl1.TabPages.Remove(AnalogControlsTab);
}
if (FeedbacksTab.Controls.Count == 0) tabControl1.TabPages.Remove(FeedbacksTab);
}
private void LoadPanels(DefaultControls cd)
{
LoadPanels(cd.AllTrollers, cd.AllTrollersAutoFire, cd.AllTrollersAnalog);
LoadPanels(cd.AllTrollers, cd.AllTrollersAutoFire, cd.AllTrollersAnalog, cd.AllTrollersFeedbacks);
}
private void LoadPanels(Config c)
{
LoadPanels(c.AllTrollers, c.AllTrollersAutoFire, c.AllTrollersAnalog);
LoadPanels(c.AllTrollers, c.AllTrollersAutoFire, c.AllTrollersAnalog, c.AllTrollersFeedbacks);
}
private void SetControllerPicture(string controlName)
@ -328,6 +343,7 @@ namespace BizHawk.Client.EmuHawk
ActOnControlCollection<ControllerConfigPanel>(NormalControlsTab, c => c.Save(_config.AllTrollers[_emulator.ControllerDefinition.Name]));
ActOnControlCollection<ControllerConfigPanel>(AutofireControlsTab, c => c.Save(_config.AllTrollersAutoFire[_emulator.ControllerDefinition.Name]));
ActOnControlCollection<AnalogBindPanel>(AnalogControlsTab, c => c.Save(_config.AllTrollersAnalog[_emulator.ControllerDefinition.Name]));
ActOnControlCollection<FeedbacksBindPanel>(FeedbacksTab, c => c.Save(_config.AllTrollersFeedbacks[_emulator.ControllerDefinition.Name]));
}
private void SaveToDefaults(DefaultControls cd)
@ -335,6 +351,7 @@ namespace BizHawk.Client.EmuHawk
ActOnControlCollection<ControllerConfigPanel>(NormalControlsTab, c => c.Save(cd.AllTrollers[_emulator.ControllerDefinition.Name]));
ActOnControlCollection<ControllerConfigPanel>(AutofireControlsTab, c => c.Save(cd.AllTrollersAutoFire[_emulator.ControllerDefinition.Name]));
ActOnControlCollection<AnalogBindPanel>(AnalogControlsTab, c => c.Save(cd.AllTrollersAnalog[_emulator.ControllerDefinition.Name]));
ActOnControlCollection<FeedbacksBindPanel>(FeedbacksTab, c => c.Save(cd.AllTrollersFeedbacks[_emulator.ControllerDefinition.Name]));
}
private static void ActOnControlCollection<T>(Control c, Action<T> proc)
@ -389,17 +406,21 @@ namespace BizHawk.Client.EmuHawk
var tb1 = GetTabControl(NormalControlsTab.Controls);
var tb2 = GetTabControl(AutofireControlsTab.Controls);
var tb3 = GetTabControl(AnalogControlsTab.Controls);
var tb4 = GetTabControl(FeedbacksTab.Controls);
int? wasTabbedPage1 = null;
int? wasTabbedPage2 = null;
int? wasTabbedPage3 = null;
int? wasTabbedPage4 = null;
if (tb1?.SelectedTab != null) { wasTabbedPage1 = tb1.SelectedIndex; }
if (tb2?.SelectedTab != null) { wasTabbedPage2 = tb2.SelectedIndex; }
if (tb3?.SelectedTab != null) { wasTabbedPage3 = tb3.SelectedIndex; }
if (tb4?.SelectedTab != null) { wasTabbedPage4 = tb4.SelectedIndex; }
NormalControlsTab.Controls.Clear();
AutofireControlsTab.Controls.Clear();
AnalogControlsTab.Controls.Clear();
FeedbacksTab.Controls.Clear();
// load panels directly from the default config.
// this means that the changes are NOT committed. so "Cancel" works right and you
@ -427,6 +448,12 @@ namespace BizHawk.Client.EmuHawk
newTb3?.SelectTab(wasTabbedPage3.Value);
}
if (wasTabbedPage4.HasValue)
{
var newTb4 = GetTabControl(FeedbacksTab.Controls);
newTb4?.SelectTab(wasTabbedPage4.Value);
}
tabControl1.ResumeLayout();
}

View File

@ -0,0 +1,127 @@
#nullable enable
using System.ComponentModel;
using System.Windows.Forms;
using BizHawk.Client.Common;
using BizHawk.Common.StringExtensions;
using BizHawk.WinForms.Controls;
namespace BizHawk.Client.EmuHawk
{
public class FeedbackBindControl : UserControl
{
private readonly Container _components = new();
/// <summary>'+'-delimited e.g. <c>"Mono"</c>, <c>"Left"</c>, <c>"Left+Right"</c></summary>
public string BoundChannels { get; private set; }
public string BoundGamepadPrefix { get; private set; }
public float Prescale { get; private set; }
public readonly string VChannelName;
public FeedbackBindControl(string vChannel, FeedbackBind existingBind, IHostInputAdapter hostInputAdapter)
{
BoundChannels = existingBind.Channel ?? string.Empty;
BoundGamepadPrefix = existingBind.GamepadPrefix ?? string.Empty;
Prescale = existingBind.IsZeroed ? 1.0f : existingBind.Prescale;
VChannelName = vChannel;
SzTextBoxEx txtBoundPrefix = new() { ReadOnly = true, Size = new(19, 19) };
ComboBox cbBoundChannel = new() { Enabled = false, Size = new(112, 24) };
void UpdateDropdownAndLabel(string newPrefix)
{
txtBoundPrefix.Text = newPrefix;
var wasSelected = (string) cbBoundChannel.SelectedItem;
cbBoundChannel.Enabled = false;
cbBoundChannel.SelectedIndex = -1;
cbBoundChannel.Items.Clear();
if (hostInputAdapter.GetHapticsChannels().TryGetValue(newPrefix, out var channels) && channels.Count != 0)
{
var hasLeft = false;
var hasRight = false;
foreach (var hostChannel in channels)
{
cbBoundChannel.Items.Add(hostChannel);
if (hostChannel == "Left") hasLeft = true;
else if (hostChannel == "Right") hasRight = true;
}
if (hasLeft && hasRight) cbBoundChannel.Items.Add("Left+Right");
cbBoundChannel.SelectedItem = wasSelected;
cbBoundChannel.Enabled = true;
}
else if (!string.IsNullOrEmpty(newPrefix))
{
cbBoundChannel.Items.Add("(none available)");
cbBoundChannel.SelectedIndex = 0;
}
}
UpdateDropdownAndLabel(BoundGamepadPrefix);
cbBoundChannel.SelectedIndexChanged += (changedSender, _)
=> BoundChannels = (string?) ((ComboBox) changedSender).SelectedItem ?? string.Empty;
SingleRowFLP flpBindReadout = new() { Controls = { txtBoundPrefix, cbBoundChannel, new LabelEx { Text = vChannel } } };
Timer timer = new(_components);
SzButtonEx btnBind = new() { Size = new(75, 23), Text = "Bind!" };
void UpdateListeningState(bool newState)
{
if (newState)
{
Input.Instance.StartListeningForAxisEvents();
timer.Start();
btnBind.Text = "Cancel!";
}
else
{
timer.Stop();
Input.Instance.StopListeningForAxisEvents();
btnBind.Text = "Bind!";
}
}
var isListening = false;
timer.Tick += (_, _) =>
{
var bindValue = Input.Instance.GetNextAxisEvent();
if (bindValue == null) return;
UpdateListeningState(isListening = false);
UpdateDropdownAndLabel(BoundGamepadPrefix = bindValue.SubstringBefore(' ') + ' ');
};
btnBind.Click += (_, _) => UpdateListeningState(isListening = !isListening);
SzButtonEx btnUnbind = new() { Size = new(75, 23), Text = "Unbind!" };
btnUnbind.Click += (_, _) => UpdateDropdownAndLabel(BoundGamepadPrefix = string.Empty);
LocSingleColumnFLP flpButtons = new() { Controls = { btnBind, btnUnbind } };
LabelEx lblPrescale = new() { Margin = new(0, 0, 0, 24) };
TransparentTrackBar tbPrescale = new() { Maximum = 20, Size = new(96, 45), TickFrequency = 5 };
tbPrescale.ValueChanged += (changedSender, _) =>
{
Prescale = ((TrackBar) changedSender).Value / 10.0f;
lblPrescale.Text = $"Pre-scaled by: {Prescale:F1}x";
};
tbPrescale.Value = (int) (Prescale * 10.0f);
LocSzSingleRowFLP flpPrescale = new() { Controls = { lblPrescale, tbPrescale }, Size = new(200, 32) };
SuspendLayout();
AutoScaleDimensions = new(6.0f, 13.0f);
AutoScaleMode = AutoScaleMode.Font;
Size = new(378, 99);
Controls.Add(new SingleColumnFLP
{
Controls =
{
flpBindReadout,
new SingleRowFLP { Controls = { flpButtons, flpPrescale } }
}
});
ResumeLayout();
}
protected override void Dispose(bool disposing)
{
if (disposing) _components.Dispose();
base.Dispose(disposing);
}
}
}

View File

@ -0,0 +1,46 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using BizHawk.Client.Common;
using BizHawk.WinForms.Controls;
namespace BizHawk.Client.EmuHawk
{
public class FeedbacksBindPanel : UserControl
{
private readonly FlowLayoutPanel _flpMain = new SingleColumnFLP();
private readonly IDictionary<string, FeedbackBind> _realConfigObject;
public FeedbacksBindPanel(IDictionary<string, FeedbackBind> realConfigObject, ICollection<string>? realConfigButtons = null)
{
_realConfigObject = realConfigObject;
_flpMain.Controls.Add(new LabelEx { Text = "To bind, click \"Bind!\", move an axis (e.g. analog stick) on the desired gamepad, and choose from the dropdown.\nNote: haptic feedback won't work if your gamepad is shown as \"J#\" or if your input method is OpenTK." });
var adapter = Input.Instance.Adapter;
foreach (var buttonName in realConfigButtons ?? realConfigObject.Keys)
{
_flpMain.Controls.Add(new FeedbackBindControl(buttonName, _realConfigObject[buttonName], adapter));
}
SuspendLayout();
Controls.Add(_flpMain);
ResumeLayout();
}
/// <param name="saveConfigObject">if non-null, save to possibly different config object than originally initialized from</param>
public void Save(IDictionary<string, FeedbackBind>? saveConfigObject = null)
{
var saveTo = saveConfigObject ?? _realConfigObject;
foreach (var c in _flpMain.Controls.OfType<FeedbackBindControl>())
{
if (string.IsNullOrEmpty(c.BoundGamepadPrefix)) continue;
foreach (var channel in c.BoundChannels.Split('+'))
{
saveTo[c.VChannelName] = new(c.BoundGamepadPrefix, channel, c.Prescale);
}
}
}
}
}

View File

@ -1,4 +1,6 @@
namespace BizHawk.Emulation.Common
using System.Collections.Generic;
namespace BizHawk.Emulation.Common
{
/// <summary>
/// A empty implementation of IController that represents the lack of