Implement vibrations (#2468)
* First working vibration implementation * Fix Infinite Rumble in SDL2Mouse * Stop ignoring one vibValues every 2 * Remove RumbleInfinity as suggested * Reworked all the vibration handle / calculation * Revert HidVibrationDevicePosition changes * Add UI to enable and tune rumble * Remove some stub logs * Add PlayerIndex in rumble debug log * Fix all requested changes * Implements hid::GetVibrationDeviceInfo * Better implements HidVibrationValue.Equals/GetHashCode * Added requested changes from code review * Last fixes from review * Update configuration file version for rebase
This commit is contained in:
parent
46ffc81d90
commit
70f79e689b
18 changed files with 468 additions and 50 deletions
|
@ -33,5 +33,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller
|
||||||
/// Controller Motion Settings
|
/// Controller Motion Settings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MotionConfigController Motion { get; set; }
|
public MotionConfigController Motion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller Rumble Settings
|
||||||
|
/// </summary>
|
||||||
|
public RumbleConfigController Rumble { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
namespace Ryujinx.Common.Configuration.Hid.Controller
|
||||||
|
{
|
||||||
|
public class RumbleConfigController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Controller Strong Rumble Multiplier
|
||||||
|
/// </summary>
|
||||||
|
public float StrongRumble { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller Weak Rumble Multiplier
|
||||||
|
/// </summary>
|
||||||
|
public float WeakRumble { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable Rumble
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableRumble { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
|
using Ryujinx.Common.Configuration.Hid;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||||
using Ryujinx.HLE.HOS.Services.Hid.Types;
|
using Ryujinx.HLE.HOS.Services.Hid.Types;
|
||||||
|
@ -20,11 +22,21 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||||
private ControllerType[] _configuredTypes;
|
private ControllerType[] _configuredTypes;
|
||||||
private KEvent[] _styleSetUpdateEvents;
|
private KEvent[] _styleSetUpdateEvents;
|
||||||
private bool[] _supportedPlayers;
|
private bool[] _supportedPlayers;
|
||||||
|
private static HidVibrationValue _neutralVibrationValue = new HidVibrationValue
|
||||||
|
{
|
||||||
|
AmplitudeLow = 0f,
|
||||||
|
FrequencyLow = 160f,
|
||||||
|
AmplitudeHigh = 0f,
|
||||||
|
FrequencyHigh = 320f
|
||||||
|
};
|
||||||
|
|
||||||
internal NpadJoyHoldType JoyHold { get; set; }
|
internal NpadJoyHoldType JoyHold { get; set; }
|
||||||
internal bool SixAxisActive = false; // TODO: link to hidserver when implemented
|
internal bool SixAxisActive = false; // TODO: link to hidserver when implemented
|
||||||
internal ControllerType SupportedStyleSets { get; set; }
|
internal ControllerType SupportedStyleSets { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<PlayerIndex, ConcurrentQueue<(HidVibrationValue, HidVibrationValue)>> RumbleQueues = new Dictionary<PlayerIndex, ConcurrentQueue<(HidVibrationValue, HidVibrationValue)>>();
|
||||||
|
public Dictionary<PlayerIndex, (HidVibrationValue, HidVibrationValue)> LastVibrationValues = new Dictionary<PlayerIndex, (HidVibrationValue, HidVibrationValue)>();
|
||||||
|
|
||||||
public NpadDevices(Switch device, bool active = true) : base(device, active)
|
public NpadDevices(Switch device, bool active = true) : base(device, active)
|
||||||
{
|
{
|
||||||
_configuredTypes = new ControllerType[MaxControllers];
|
_configuredTypes = new ControllerType[MaxControllers];
|
||||||
|
@ -596,5 +608,49 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||||
WriteNewSixInputEntry(ref currentNpad.JoyLeftSixAxisSensor, ref newState);
|
WriteNewSixInputEntry(ref currentNpad.JoyLeftSixAxisSensor, ref newState);
|
||||||
WriteNewSixInputEntry(ref currentNpad.JoyRightSixAxisSensor, ref newState);
|
WriteNewSixInputEntry(ref currentNpad.JoyRightSixAxisSensor, ref newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpdateRumbleQueue(PlayerIndex index, Dictionary<byte, HidVibrationValue> dualVibrationValues)
|
||||||
|
{
|
||||||
|
if (RumbleQueues.TryGetValue(index, out ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> currentQueue))
|
||||||
|
{
|
||||||
|
if (!dualVibrationValues.TryGetValue(0, out HidVibrationValue leftVibrationValue))
|
||||||
|
{
|
||||||
|
leftVibrationValue = _neutralVibrationValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dualVibrationValues.TryGetValue(1, out HidVibrationValue rightVibrationValue))
|
||||||
|
{
|
||||||
|
rightVibrationValue = _neutralVibrationValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LastVibrationValues.TryGetValue(index, out (HidVibrationValue, HidVibrationValue) dualVibrationValue) || !leftVibrationValue.Equals(dualVibrationValue.Item1) || !rightVibrationValue.Equals(dualVibrationValue.Item2))
|
||||||
|
{
|
||||||
|
currentQueue.Enqueue((leftVibrationValue, rightVibrationValue));
|
||||||
|
|
||||||
|
LastVibrationValues[index] = (leftVibrationValue, rightVibrationValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HidVibrationValue GetLastVibrationValue(PlayerIndex index, byte position)
|
||||||
|
{
|
||||||
|
if (!LastVibrationValues.TryGetValue(index, out (HidVibrationValue, HidVibrationValue) dualVibrationValue))
|
||||||
|
{
|
||||||
|
return _neutralVibrationValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (position == 0) ? dualVibrationValue.Item1 : dualVibrationValue.Item2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> GetRumbleQueue(PlayerIndex index)
|
||||||
|
{
|
||||||
|
if (!RumbleQueues.TryGetValue(index, out ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> rumbleQueue))
|
||||||
|
{
|
||||||
|
rumbleQueue = new ConcurrentQueue<(HidVibrationValue, HidVibrationValue)>();
|
||||||
|
_device.Hid.Npads.RumbleQueues[index] = rumbleQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rumbleQueue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Hid
|
||||||
|
{
|
||||||
|
public struct HidVibrationDeviceHandle
|
||||||
|
{
|
||||||
|
public byte DeviceType;
|
||||||
|
public byte PlayerId;
|
||||||
|
public byte Position;
|
||||||
|
public byte Reserved;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
public enum HidVibrationDeviceType
|
public enum HidVibrationDeviceType
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
LinearResonantActuator
|
LinearResonantActuator,
|
||||||
|
GcErm
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
namespace Ryujinx.HLE.HOS.Services.Hid
|
using Ryujinx.HLE.HOS.Tamper;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Hid
|
||||||
{
|
{
|
||||||
public struct HidVibrationValue
|
public struct HidVibrationValue
|
||||||
{
|
{
|
||||||
|
@ -6,5 +9,17 @@
|
||||||
public float FrequencyLow;
|
public float FrequencyLow;
|
||||||
public float AmplitudeHigh;
|
public float AmplitudeHigh;
|
||||||
public float FrequencyHigh;
|
public float FrequencyHigh;
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is HidVibrationValue value &&
|
||||||
|
AmplitudeLow == value.AmplitudeLow &&
|
||||||
|
AmplitudeHigh == value.AmplitudeHigh;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(AmplitudeLow, AmplitudeHigh);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.HOS.Ipc;
|
using Ryujinx.HLE.HOS.Ipc;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Common;
|
using Ryujinx.HLE.HOS.Kernel.Common;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||||
using Ryujinx.HLE.HOS.Services.Hid.HidServer;
|
using Ryujinx.HLE.HOS.Services.Hid.HidServer;
|
||||||
using Ryujinx.HLE.HOS.Services.Hid.Types;
|
using Ryujinx.HLE.HOS.Services.Hid.Types;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
@ -37,7 +40,6 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||||
|
|
||||||
private HidSensorFusionParameters _sensorFusionParams;
|
private HidSensorFusionParameters _sensorFusionParams;
|
||||||
private HidAccelerometerParameters _accelerometerParams;
|
private HidAccelerometerParameters _accelerometerParams;
|
||||||
private HidVibrationValue _vibrationValue;
|
|
||||||
|
|
||||||
public IHidServer(ServiceCtx context) : base(context.Device.System.HidServer)
|
public IHidServer(ServiceCtx context) : base(context.Device.System.HidServer)
|
||||||
{
|
{
|
||||||
|
@ -52,7 +54,6 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||||
|
|
||||||
_sensorFusionParams = new HidSensorFusionParameters();
|
_sensorFusionParams = new HidSensorFusionParameters();
|
||||||
_accelerometerParams = new HidAccelerometerParameters();
|
_accelerometerParams = new HidAccelerometerParameters();
|
||||||
_vibrationValue = new HidVibrationValue();
|
|
||||||
|
|
||||||
// TODO: signal event at right place
|
// TODO: signal event at right place
|
||||||
_xpadIdEvent.ReadableEvent.Signal();
|
_xpadIdEvent.ReadableEvent.Signal();
|
||||||
|
@ -1025,29 +1026,78 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||||
// GetVibrationDeviceInfo(nn::hid::VibrationDeviceHandle) -> nn::hid::VibrationDeviceInfo
|
// GetVibrationDeviceInfo(nn::hid::VibrationDeviceHandle) -> nn::hid::VibrationDeviceInfo
|
||||||
public ResultCode GetVibrationDeviceInfo(ServiceCtx context)
|
public ResultCode GetVibrationDeviceInfo(ServiceCtx context)
|
||||||
{
|
{
|
||||||
int vibrationDeviceHandle = context.RequestData.ReadInt32();
|
HidVibrationDeviceHandle deviceHandle = context.RequestData.ReadStruct<HidVibrationDeviceHandle>();
|
||||||
|
NpadStyleIndex deviceType = (NpadStyleIndex)deviceHandle.DeviceType;
|
||||||
|
NpadIdType npadIdType = (NpadIdType)deviceHandle.PlayerId;
|
||||||
|
|
||||||
|
if (deviceType < NpadStyleIndex.System || deviceType >= NpadStyleIndex.FullKey)
|
||||||
|
{
|
||||||
|
if (npadIdType >= (NpadIdType.Player8 + 1) && npadIdType != NpadIdType.Handheld && npadIdType != NpadIdType.Unknown)
|
||||||
|
{
|
||||||
|
return ResultCode.InvalidNpadIdType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceHandle.Position > 1)
|
||||||
|
{
|
||||||
|
return ResultCode.InvalidDeviceIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
HidVibrationDeviceType vibrationDeviceType = HidVibrationDeviceType.None;
|
||||||
|
|
||||||
|
if (Enum.IsDefined(typeof(NpadStyleIndex), deviceType))
|
||||||
|
{
|
||||||
|
vibrationDeviceType = HidVibrationDeviceType.LinearResonantActuator;
|
||||||
|
}
|
||||||
|
else if ((uint)deviceType == 8)
|
||||||
|
{
|
||||||
|
vibrationDeviceType = HidVibrationDeviceType.GcErm;
|
||||||
|
}
|
||||||
|
|
||||||
|
HidVibrationDevicePosition vibrationDevicePosition = HidVibrationDevicePosition.None;
|
||||||
|
|
||||||
|
if (vibrationDeviceType == HidVibrationDeviceType.LinearResonantActuator)
|
||||||
|
{
|
||||||
|
if (deviceHandle.Position == 0)
|
||||||
|
{
|
||||||
|
vibrationDevicePosition = HidVibrationDevicePosition.Left;
|
||||||
|
}
|
||||||
|
else if (deviceHandle.Position == 1)
|
||||||
|
{
|
||||||
|
vibrationDevicePosition = HidVibrationDevicePosition.Right;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(deviceHandle.Position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HidVibrationDeviceValue deviceInfo = new HidVibrationDeviceValue
|
HidVibrationDeviceValue deviceInfo = new HidVibrationDeviceValue
|
||||||
{
|
{
|
||||||
DeviceType = HidVibrationDeviceType.None,
|
DeviceType = vibrationDeviceType,
|
||||||
Position = HidVibrationDevicePosition.None
|
Position = vibrationDevicePosition
|
||||||
};
|
};
|
||||||
|
|
||||||
context.ResponseData.Write((int)deviceInfo.DeviceType);
|
context.ResponseData.WriteStruct(deviceInfo);
|
||||||
context.ResponseData.Write((int)deviceInfo.Position);
|
|
||||||
|
|
||||||
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { vibrationDeviceHandle, deviceInfo.DeviceType, deviceInfo.Position });
|
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ResultCode.InvalidNpadDeviceType;
|
||||||
|
}
|
||||||
|
|
||||||
[CommandHipc(201)]
|
[CommandHipc(201)]
|
||||||
// SendVibrationValue(nn::hid::VibrationDeviceHandle, nn::hid::VibrationValue, nn::applet::AppletResourceUserId)
|
// SendVibrationValue(nn::hid::VibrationDeviceHandle, nn::hid::VibrationValue, nn::applet::AppletResourceUserId)
|
||||||
public ResultCode SendVibrationValue(ServiceCtx context)
|
public ResultCode SendVibrationValue(ServiceCtx context)
|
||||||
{
|
{
|
||||||
int vibrationDeviceHandle = context.RequestData.ReadInt32();
|
HidVibrationDeviceHandle deviceHandle = new HidVibrationDeviceHandle
|
||||||
|
{
|
||||||
|
DeviceType = context.RequestData.ReadByte(),
|
||||||
|
PlayerId = context.RequestData.ReadByte(),
|
||||||
|
Position = context.RequestData.ReadByte(),
|
||||||
|
Reserved = context.RequestData.ReadByte()
|
||||||
|
};
|
||||||
|
|
||||||
_vibrationValue = new HidVibrationValue
|
HidVibrationValue vibrationValue = new HidVibrationValue
|
||||||
{
|
{
|
||||||
AmplitudeLow = context.RequestData.ReadSingle(),
|
AmplitudeLow = context.RequestData.ReadSingle(),
|
||||||
FrequencyLow = context.RequestData.ReadSingle(),
|
FrequencyLow = context.RequestData.ReadSingle(),
|
||||||
|
@ -1057,14 +1107,11 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||||
|
|
||||||
long appletResourceUserId = context.RequestData.ReadInt64();
|
long appletResourceUserId = context.RequestData.ReadInt64();
|
||||||
|
|
||||||
Logger.Debug?.PrintStub(LogClass.ServiceHid, new {
|
Dictionary<byte, HidVibrationValue> dualVibrationValues = new Dictionary<byte, HidVibrationValue>();
|
||||||
appletResourceUserId,
|
|
||||||
vibrationDeviceHandle,
|
dualVibrationValues[deviceHandle.Position] = vibrationValue;
|
||||||
_vibrationValue.AmplitudeLow,
|
|
||||||
_vibrationValue.FrequencyLow,
|
context.Device.Hid.Npads.UpdateRumbleQueue((PlayerIndex)deviceHandle.PlayerId, dualVibrationValues);
|
||||||
_vibrationValue.AmplitudeHigh,
|
|
||||||
_vibrationValue.FrequencyHigh
|
|
||||||
});
|
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
@ -1073,22 +1120,22 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||||
// GetActualVibrationValue(nn::hid::VibrationDeviceHandle, nn::applet::AppletResourceUserId) -> nn::hid::VibrationValue
|
// GetActualVibrationValue(nn::hid::VibrationDeviceHandle, nn::applet::AppletResourceUserId) -> nn::hid::VibrationValue
|
||||||
public ResultCode GetActualVibrationValue(ServiceCtx context)
|
public ResultCode GetActualVibrationValue(ServiceCtx context)
|
||||||
{
|
{
|
||||||
int vibrationDeviceHandle = context.RequestData.ReadInt32();
|
HidVibrationDeviceHandle deviceHandle = new HidVibrationDeviceHandle
|
||||||
|
{
|
||||||
|
DeviceType = context.RequestData.ReadByte(),
|
||||||
|
PlayerId = context.RequestData.ReadByte(),
|
||||||
|
Position = context.RequestData.ReadByte(),
|
||||||
|
Reserved = context.RequestData.ReadByte()
|
||||||
|
};
|
||||||
|
|
||||||
long appletResourceUserId = context.RequestData.ReadInt64();
|
long appletResourceUserId = context.RequestData.ReadInt64();
|
||||||
|
|
||||||
context.ResponseData.Write(_vibrationValue.AmplitudeLow);
|
HidVibrationValue vibrationValue = context.Device.Hid.Npads.GetLastVibrationValue((PlayerIndex)deviceHandle.PlayerId, deviceHandle.Position);
|
||||||
context.ResponseData.Write(_vibrationValue.FrequencyLow);
|
|
||||||
context.ResponseData.Write(_vibrationValue.AmplitudeHigh);
|
|
||||||
context.ResponseData.Write(_vibrationValue.FrequencyHigh);
|
|
||||||
|
|
||||||
Logger.Stub?.PrintStub(LogClass.ServiceHid, new {
|
context.ResponseData.Write(vibrationValue.AmplitudeLow);
|
||||||
appletResourceUserId,
|
context.ResponseData.Write(vibrationValue.FrequencyLow);
|
||||||
vibrationDeviceHandle,
|
context.ResponseData.Write(vibrationValue.AmplitudeHigh);
|
||||||
_vibrationValue.AmplitudeLow,
|
context.ResponseData.Write(vibrationValue.FrequencyHigh);
|
||||||
_vibrationValue.FrequencyLow,
|
|
||||||
_vibrationValue.AmplitudeHigh,
|
|
||||||
_vibrationValue.FrequencyHigh
|
|
||||||
});
|
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
@ -1138,13 +1185,31 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||||
|
|
||||||
context.Memory.Read(context.Request.PtrBuff[1].Position, vibrationValueBuffer);
|
context.Memory.Read(context.Request.PtrBuff[1].Position, vibrationValueBuffer);
|
||||||
|
|
||||||
// TODO: Read all handles and values from buffer.
|
Span<HidVibrationDeviceHandle> deviceHandles = MemoryMarshal.Cast<byte, HidVibrationDeviceHandle>(vibrationDeviceHandleBuffer);
|
||||||
|
Span<HidVibrationValue> vibrationValues = MemoryMarshal.Cast<byte, HidVibrationValue>(vibrationValueBuffer);
|
||||||
|
|
||||||
Logger.Debug?.PrintStub(LogClass.ServiceHid, new {
|
if (!deviceHandles.IsEmpty && vibrationValues.Length == deviceHandles.Length)
|
||||||
appletResourceUserId,
|
{
|
||||||
VibrationDeviceHandleBufferLength = vibrationDeviceHandleBuffer.Length,
|
Dictionary<byte, HidVibrationValue> dualVibrationValues = new Dictionary<byte, HidVibrationValue>();
|
||||||
VibrationValueBufferLength = vibrationValueBuffer.Length
|
PlayerIndex currentIndex = (PlayerIndex)deviceHandles[0].PlayerId;
|
||||||
});
|
|
||||||
|
for (int deviceCounter = 0; deviceCounter < deviceHandles.Length; deviceCounter++)
|
||||||
|
{
|
||||||
|
PlayerIndex index = (PlayerIndex)deviceHandles[deviceCounter].PlayerId;
|
||||||
|
byte position = deviceHandles[deviceCounter].Position;
|
||||||
|
|
||||||
|
if (index != currentIndex || dualVibrationValues.Count == 2)
|
||||||
|
{
|
||||||
|
context.Device.Hid.Npads.UpdateRumbleQueue(currentIndex, dualVibrationValues);
|
||||||
|
dualVibrationValues = new Dictionary<byte, HidVibrationValue>();
|
||||||
|
}
|
||||||
|
|
||||||
|
dualVibrationValues[position] = vibrationValues[deviceCounter];
|
||||||
|
currentIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Device.Hid.Npads.UpdateRumbleQueue(currentIndex, dualVibrationValues);
|
||||||
|
}
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
|
|
||||||
Success = 0,
|
Success = 0,
|
||||||
|
|
||||||
InvalidNpadIdType = (710 << ErrorCodeShift) | ModuleId
|
InvalidNpadDeviceType = (122 << ErrorCodeShift) | ModuleId,
|
||||||
|
InvalidNpadIdType = (123 << ErrorCodeShift) | ModuleId,
|
||||||
|
InvalidDeviceIndex = (124 << ErrorCodeShift) | ModuleId,
|
||||||
|
InvalidBufferSize = (131 << ErrorCodeShift) | ModuleId
|
||||||
}
|
}
|
||||||
}
|
}
|
13
Ryujinx.HLE/HOS/Services/Hid/Types/Npad/NpadStyleIndex.cs
Normal file
13
Ryujinx.HLE/HOS/Services/Hid/Types/Npad/NpadStyleIndex.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Hid
|
||||||
|
{
|
||||||
|
public enum NpadStyleIndex : byte
|
||||||
|
{
|
||||||
|
FullKey = 3,
|
||||||
|
Handheld = 4,
|
||||||
|
JoyDual = 5,
|
||||||
|
JoyLeft = 6,
|
||||||
|
JoyRight = 7,
|
||||||
|
SystemExt = 32,
|
||||||
|
System = 33
|
||||||
|
}
|
||||||
|
}
|
|
@ -236,6 +236,12 @@ namespace Ryujinx.Headless.SDL2
|
||||||
EnableMotion = true,
|
EnableMotion = true,
|
||||||
Sensitivity = 100,
|
Sensitivity = 100,
|
||||||
GyroDeadzone = 1,
|
GyroDeadzone = 1,
|
||||||
|
},
|
||||||
|
Rumble = new RumbleConfigController
|
||||||
|
{
|
||||||
|
StrongRumble = 1f,
|
||||||
|
WeakRumble = 1f,
|
||||||
|
EnableRumble = false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using Ryujinx.Common.Configuration.Hid;
|
using Ryujinx.Common.Configuration.Hid;
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
@ -151,9 +152,20 @@ namespace Ryujinx.Input.SDL2
|
||||||
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
|
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
|
||||||
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
|
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
|
||||||
|
|
||||||
|
if (durationMs == uint.MaxValue)
|
||||||
|
{
|
||||||
|
SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, SDL_HAPTIC_INFINITY);
|
||||||
|
}
|
||||||
|
else if (durationMs > SDL_HAPTIC_INFINITY)
|
||||||
|
{
|
||||||
|
Logger.Error?.Print(LogClass.Hid, $"Unsupported rumble duration {durationMs}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs);
|
SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Vector3 GetMotionData(MotionInputId inputId)
|
public Vector3 GetMotionData(MotionInputId inputId)
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,8 +2,11 @@
|
||||||
using Ryujinx.Common.Configuration.Hid;
|
using Ryujinx.Common.Configuration.Hid;
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.HOS.Services.Hid;
|
using Ryujinx.HLE.HOS.Services.Hid;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
@ -534,5 +537,29 @@ namespace Ryujinx.Input.HLE
|
||||||
{
|
{
|
||||||
Dispose(true);
|
Dispose(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpdateRumble(ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> queue)
|
||||||
|
{
|
||||||
|
if (queue.TryDequeue(out (HidVibrationValue, HidVibrationValue) dualVibrationValue))
|
||||||
|
{
|
||||||
|
if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble)
|
||||||
|
{
|
||||||
|
HidVibrationValue leftVibrationValue = dualVibrationValue.Item1;
|
||||||
|
HidVibrationValue rightVibrationValue = dualVibrationValue.Item2;
|
||||||
|
|
||||||
|
float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble));
|
||||||
|
float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble));
|
||||||
|
|
||||||
|
_gamepad.Rumble(low, high, uint.MaxValue);
|
||||||
|
|
||||||
|
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
|
||||||
|
$"L.low.amp={leftVibrationValue.AmplitudeLow}, " +
|
||||||
|
$"L.high.amp={leftVibrationValue.AmplitudeHigh}, " +
|
||||||
|
$"R.low.amp={rightVibrationValue.AmplitudeLow}, " +
|
||||||
|
$"R.high.amp={rightVibrationValue.AmplitudeHigh} " +
|
||||||
|
$"--> ({low}, {high})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||||
using Ryujinx.HLE.HOS.Services.Hid;
|
using Ryujinx.HLE.HOS.Services.Hid;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
@ -167,6 +168,7 @@ namespace Ryujinx.Input.HLE
|
||||||
(SixAxisInput, SixAxisInput) motionState = default;
|
(SixAxisInput, SixAxisInput) motionState = default;
|
||||||
|
|
||||||
NpadController controller = _controllers[(int)inputConfig.PlayerIndex];
|
NpadController controller = _controllers[(int)inputConfig.PlayerIndex];
|
||||||
|
Ryujinx.HLE.HOS.Services.Hid.PlayerIndex playerIndex = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
|
||||||
|
|
||||||
bool isJoyconPair = false;
|
bool isJoyconPair = false;
|
||||||
|
|
||||||
|
@ -177,6 +179,7 @@ namespace Ryujinx.Input.HLE
|
||||||
|
|
||||||
controller.UpdateUserConfiguration(inputConfig);
|
controller.UpdateUserConfiguration(inputConfig);
|
||||||
controller.Update();
|
controller.Update();
|
||||||
|
controller.UpdateRumble(_device.Hid.Npads.GetRumbleQueue(playerIndex));
|
||||||
|
|
||||||
inputState = controller.GetHLEInputState();
|
inputState = controller.GetHLEInputState();
|
||||||
|
|
||||||
|
@ -199,15 +202,15 @@ namespace Ryujinx.Input.HLE
|
||||||
motionState.Item1.Orientation = new float[9];
|
motionState.Item1.Orientation = new float[9];
|
||||||
}
|
}
|
||||||
|
|
||||||
inputState.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
|
inputState.PlayerId = playerIndex;
|
||||||
motionState.Item1.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
|
motionState.Item1.PlayerId = playerIndex;
|
||||||
|
|
||||||
hleInputStates.Add(inputState);
|
hleInputStates.Add(inputState);
|
||||||
hleMotionStates.Add(motionState.Item1);
|
hleMotionStates.Add(motionState.Item1);
|
||||||
|
|
||||||
if (isJoyconPair && !motionState.Item2.Equals(default))
|
if (isJoyconPair && !motionState.Item2.Equals(default))
|
||||||
{
|
{
|
||||||
motionState.Item2.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
|
motionState.Item2.PlayerId = playerIndex;
|
||||||
|
|
||||||
hleMotionStates.Add(motionState.Item2);
|
hleMotionStates.Add(motionState.Item2);
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ namespace Ryujinx.Input
|
||||||
void SetConfiguration(InputConfig configuration);
|
void SetConfiguration(InputConfig configuration);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts a rumble effect on the gampead.
|
/// Starts a rumble effect on the gamepad.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param>
|
/// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param>
|
||||||
/// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param>
|
/// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param>
|
||||||
|
|
|
@ -14,7 +14,7 @@ namespace Ryujinx.Configuration
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current version of the file format
|
/// The current version of the file format
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int CurrentVersion = 29;
|
public const int CurrentVersion = 30;
|
||||||
|
|
||||||
public int Version { get; set; }
|
public int Version { get; set; }
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Configuration.Hid;
|
using Ryujinx.Common.Configuration.Hid;
|
||||||
|
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Configuration.System;
|
using Ryujinx.Configuration.System;
|
||||||
|
@ -874,6 +875,26 @@ namespace Ryujinx.Configuration
|
||||||
configurationFileUpdated = true;
|
configurationFileUpdated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (configurationFileFormat.Version < 30)
|
||||||
|
{
|
||||||
|
Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30.");
|
||||||
|
|
||||||
|
foreach(InputConfig config in configurationFileFormat.InputConfig)
|
||||||
|
{
|
||||||
|
if (config is StandardControllerInputConfig controllerConfig)
|
||||||
|
{
|
||||||
|
controllerConfig.Rumble = new RumbleConfigController
|
||||||
|
{
|
||||||
|
EnableRumble = false,
|
||||||
|
StrongRumble = 1f,
|
||||||
|
WeakRumble = 1f
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configurationFileUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
|
Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
|
||||||
Graphics.ResScale.Value = configurationFileFormat.ResScale;
|
Graphics.ResScale.Value = configurationFileFormat.ResScale;
|
||||||
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
|
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
|
||||||
|
|
|
@ -34,6 +34,8 @@ namespace Ryujinx.Ui.Windows
|
||||||
private bool _isWaitingForInput;
|
private bool _isWaitingForInput;
|
||||||
|
|
||||||
#pragma warning disable CS0649, IDE0044
|
#pragma warning disable CS0649, IDE0044
|
||||||
|
[GUI] Adjustment _controllerStrongRumble;
|
||||||
|
[GUI] Adjustment _controllerWeakRumble;
|
||||||
[GUI] Adjustment _controllerDeadzoneLeft;
|
[GUI] Adjustment _controllerDeadzoneLeft;
|
||||||
[GUI] Adjustment _controllerDeadzoneRight;
|
[GUI] Adjustment _controllerDeadzoneRight;
|
||||||
[GUI] Adjustment _controllerTriggerThreshold;
|
[GUI] Adjustment _controllerTriggerThreshold;
|
||||||
|
@ -99,6 +101,8 @@ namespace Ryujinx.Ui.Windows
|
||||||
[GUI] ToggleButton _rSl;
|
[GUI] ToggleButton _rSl;
|
||||||
[GUI] ToggleButton _rSr;
|
[GUI] ToggleButton _rSr;
|
||||||
[GUI] Image _controllerImage;
|
[GUI] Image _controllerImage;
|
||||||
|
[GUI] CheckButton _enableRumble;
|
||||||
|
[GUI] Box _rumbleBox;
|
||||||
#pragma warning restore CS0649, IDE0044
|
#pragma warning restore CS0649, IDE0044
|
||||||
|
|
||||||
private MainWindow _mainWindow;
|
private MainWindow _mainWindow;
|
||||||
|
@ -314,6 +318,7 @@ namespace Ryujinx.Ui.Windows
|
||||||
_deadZoneRightBox.Hide();
|
_deadZoneRightBox.Hide();
|
||||||
_triggerThresholdBox.Hide();
|
_triggerThresholdBox.Hide();
|
||||||
_motionBox.Hide();
|
_motionBox.Hide();
|
||||||
|
_rumbleBox.Hide();
|
||||||
}
|
}
|
||||||
else if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("controller"))
|
else if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("controller"))
|
||||||
{
|
{
|
||||||
|
@ -407,6 +412,8 @@ namespace Ryujinx.Ui.Windows
|
||||||
_zR.Label = "Unbound";
|
_zR.Label = "Unbound";
|
||||||
_rSl.Label = "Unbound";
|
_rSl.Label = "Unbound";
|
||||||
_rSr.Label = "Unbound";
|
_rSr.Label = "Unbound";
|
||||||
|
_controllerStrongRumble.Value = 1;
|
||||||
|
_controllerWeakRumble.Value = 1;
|
||||||
_controllerDeadzoneLeft.Value = 0;
|
_controllerDeadzoneLeft.Value = 0;
|
||||||
_controllerDeadzoneRight.Value = 0;
|
_controllerDeadzoneRight.Value = 0;
|
||||||
_controllerTriggerThreshold.Value = 0;
|
_controllerTriggerThreshold.Value = 0;
|
||||||
|
@ -419,6 +426,7 @@ namespace Ryujinx.Ui.Windows
|
||||||
_gyroDeadzone.Value = 1;
|
_gyroDeadzone.Value = 1;
|
||||||
_dsuServerHost.Buffer.Text = "";
|
_dsuServerHost.Buffer.Text = "";
|
||||||
_dsuServerPort.Buffer.Text = "";
|
_dsuServerPort.Buffer.Text = "";
|
||||||
|
_enableRumble.Active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetValues(InputConfig config)
|
private void SetValues(InputConfig config)
|
||||||
|
@ -497,6 +505,9 @@ namespace Ryujinx.Ui.Windows
|
||||||
_zR.Label = controllerConfig.RightJoycon.ButtonZr.ToString();
|
_zR.Label = controllerConfig.RightJoycon.ButtonZr.ToString();
|
||||||
_rSl.Label = controllerConfig.RightJoycon.ButtonSl.ToString();
|
_rSl.Label = controllerConfig.RightJoycon.ButtonSl.ToString();
|
||||||
_rSr.Label = controllerConfig.RightJoycon.ButtonSr.ToString();
|
_rSr.Label = controllerConfig.RightJoycon.ButtonSr.ToString();
|
||||||
|
_controllerStrongRumble.Value = controllerConfig.Rumble.StrongRumble;
|
||||||
|
_controllerWeakRumble.Value = controllerConfig.Rumble.WeakRumble;
|
||||||
|
_enableRumble.Active = controllerConfig.Rumble.EnableRumble;
|
||||||
_controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft;
|
_controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft;
|
||||||
_controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight;
|
_controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight;
|
||||||
_controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold;
|
_controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold;
|
||||||
|
@ -706,7 +717,13 @@ namespace Ryujinx.Ui.Windows
|
||||||
InvertStickY = _invertRStickY.Active,
|
InvertStickY = _invertRStickY.Active,
|
||||||
StickButton = rStickButton,
|
StickButton = rStickButton,
|
||||||
},
|
},
|
||||||
Motion = motionConfig
|
Motion = motionConfig,
|
||||||
|
Rumble = new RumbleConfigController
|
||||||
|
{
|
||||||
|
StrongRumble = (float)_controllerStrongRumble.Value,
|
||||||
|
WeakRumble = (float)_controllerWeakRumble.Value,
|
||||||
|
EnableRumble = _enableRumble.Active
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1045,6 +1062,12 @@ namespace Ryujinx.Ui.Windows
|
||||||
EnableMotion = true,
|
EnableMotion = true,
|
||||||
Sensitivity = 100,
|
Sensitivity = 100,
|
||||||
GyroDeadzone = 1,
|
GyroDeadzone = 1,
|
||||||
|
},
|
||||||
|
Rumble = new RumbleConfigController
|
||||||
|
{
|
||||||
|
StrongRumble = 1f,
|
||||||
|
WeakRumble = 1f,
|
||||||
|
EnableRumble = false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,20 @@
|
||||||
<property name="step_increment">1</property>
|
<property name="step_increment">1</property>
|
||||||
<property name="page_increment">4</property>
|
<property name="page_increment">4</property>
|
||||||
</object>
|
</object>
|
||||||
|
<object class="GtkAdjustment" id="_controllerStrongRumble">
|
||||||
|
<property name="lower">0.1</property>
|
||||||
|
<property name="upper">10</property>
|
||||||
|
<property name="value">1.0</property>
|
||||||
|
<property name="step_increment">0.1</property>
|
||||||
|
<property name="page_increment">1.0</property>
|
||||||
|
</object>
|
||||||
|
<object class="GtkAdjustment" id="_controllerWeakRumble">
|
||||||
|
<property name="lower">0.1</property>
|
||||||
|
<property name="upper">10</property>
|
||||||
|
<property name="value">1.0</property>
|
||||||
|
<property name="step_increment">0.1</property>
|
||||||
|
<property name="page_increment">1.0</property>
|
||||||
|
</object>
|
||||||
<object class="GtkAdjustment" id="_controllerDeadzoneLeft">
|
<object class="GtkAdjustment" id="_controllerDeadzoneLeft">
|
||||||
<property name="upper">1</property>
|
<property name="upper">1</property>
|
||||||
<property name="value">0.050000000000000003</property>
|
<property name="value">0.050000000000000003</property>
|
||||||
|
@ -1249,6 +1263,130 @@
|
||||||
<property name="position">1</property>
|
<property name="position">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="_rumbleBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="margin_top">10</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="margin_top">10</property>
|
||||||
|
<property name="margin_bottom">5</property>
|
||||||
|
<property name="label" translatable="yes">Rumble</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="bold"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkCheckButton" id="_enableRumble">
|
||||||
|
<property name="label" translatable="yes">Enable</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="receives_default">False</property>
|
||||||
|
<property name="draw_indicator">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="_StrongMultiBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="margin_top">10</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="label" translatable="yes">Strong rumble multiplier</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScale">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="adjustment">_controllerStrongRumble</property>
|
||||||
|
<property name="round_digits">1</property>
|
||||||
|
<property name="digits">1</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="_WeakMultiBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="margin_top">10</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="label" translatable="yes">Weak rumble multiplier</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">0</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScale">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="adjustment">_controllerWeakRumble</property>
|
||||||
|
<property name="round_digits">1</property>
|
||||||
|
<property name="digits">1</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
|
|
Reference in a new issue