mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
fix: Race condition between GamepadPoll detour and ImGui setup
Ideally, we would use (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0 for testing in the GamepadPollDetour whether ImGui should handle gamepad controls or not. However, this has a race condition during load with the detour which sets up ImGui and throws if our detour gets called before the other, so we opt-in for a dedicated boolean.
This commit is contained in:
parent
9a9aae3db7
commit
e4521c0100
2 changed files with 78 additions and 46 deletions
|
|
@ -3,6 +3,7 @@
|
|||
using Dalamud.Game.ClientState.Structs;
|
||||
using Dalamud.Hooking;
|
||||
using ImGuiNET;
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Game.ClientState
|
||||
{
|
||||
|
|
@ -28,6 +29,9 @@ namespace Dalamud.Game.ClientState
|
|||
/// <param name="resolver">Resolver knowing the pointer to the GamepadPoll function.</param>
|
||||
public GamepadState(ClientStateAddressResolver resolver)
|
||||
{
|
||||
#if DEBUG
|
||||
Log.Verbose("GamepadPoll address {GamepadPoll}", resolver.GamepadPoll);
|
||||
#endif
|
||||
this.gamepadPoll = new Hook<ControllerPoll>(
|
||||
resolver.GamepadPoll,
|
||||
(ControllerPoll)this.GamepadPollDetour);
|
||||
|
|
@ -118,10 +122,21 @@ namespace Dalamud.Game.ClientState
|
|||
/// </summary>
|
||||
internal ushort ButtonsRepeat { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether detour should block gamepad input for game.
|
||||
///
|
||||
/// Ideally, we would use
|
||||
/// (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0
|
||||
/// but this has a race condition during load with the detour which sets up ImGui
|
||||
/// and throws if our detour gets called before the other.
|
||||
/// </summary>
|
||||
internal bool NavEnableGamepad { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether <paramref name="button"/> has been pressed.
|
||||
///
|
||||
/// Only true on first frame of the press.
|
||||
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
|
||||
/// </summary>
|
||||
/// <param name="button">The button to check for.</param>
|
||||
/// <returns>1 if pressed, 0 otherwise.</returns>
|
||||
|
|
@ -131,6 +146,7 @@ namespace Dalamud.Game.ClientState
|
|||
/// Gets whether <paramref name="button"/> is being pressed.
|
||||
///
|
||||
/// True in intervals if button is held down.
|
||||
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
|
||||
/// </summary>
|
||||
/// <param name="button">The button to check for.</param>
|
||||
/// <returns>1 if still pressed during interval, 0 otherwise or in between intervals.</returns>
|
||||
|
|
@ -140,6 +156,7 @@ namespace Dalamud.Game.ClientState
|
|||
/// Gets whether <paramref name="button"/> has been released.
|
||||
///
|
||||
/// Only true the frame after release.
|
||||
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
|
||||
/// </summary>
|
||||
/// <param name="button">The button to check for.</param>
|
||||
/// <returns>1 if released, 0 otherwise.</returns>
|
||||
|
|
@ -173,56 +190,68 @@ namespace Dalamud.Game.ClientState
|
|||
|
||||
private int GamepadPollDetour(IntPtr gamepadInput)
|
||||
{
|
||||
#if DEBUG
|
||||
this.GamepadInput = gamepadInput;
|
||||
#endif
|
||||
var original = this.gamepadPoll.Original(gamepadInput);
|
||||
var input = (GamepadInput*)gamepadInput;
|
||||
this.leftStickX = input->LeftStickX;
|
||||
this.leftStickY = input->LeftStickY;
|
||||
this.rightStickX = input->RightStickX;
|
||||
this.rightStickY = input->RightStickY;
|
||||
this.ButtonsRaw = input->ButtonsRaw;
|
||||
this.ButtonsPressed = input->ButtonsPressed;
|
||||
this.ButtonsReleased = input->ButtonsReleased;
|
||||
this.ButtonsRepeat = input->ButtonsRepeat;
|
||||
|
||||
if ((ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0)
|
||||
try
|
||||
{
|
||||
input->LeftStickX = 0;
|
||||
input->LeftStickY = 0;
|
||||
input->RightStickX = 0;
|
||||
input->RightStickY = 0;
|
||||
#if DEBUG
|
||||
this.GamepadInput = gamepadInput;
|
||||
#endif
|
||||
var input = (GamepadInput*)gamepadInput;
|
||||
this.leftStickX = input->LeftStickX;
|
||||
this.leftStickY = input->LeftStickY;
|
||||
this.rightStickX = input->RightStickX;
|
||||
this.rightStickY = input->RightStickY;
|
||||
this.ButtonsRaw = input->ButtonsRaw;
|
||||
this.ButtonsPressed = input->ButtonsPressed;
|
||||
this.ButtonsReleased = input->ButtonsReleased;
|
||||
this.ButtonsRepeat = input->ButtonsRepeat;
|
||||
|
||||
// NOTE (Chiv) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased`
|
||||
// and `ButtonRepeat` as the game uses the RAW input to determine those (apparently).
|
||||
// It does block, however, all input to the game.
|
||||
// Leaving `ButtonsRaw` as it is and only zeroing the other leaves e.g. long-hold L2/R2
|
||||
// and the digipad (in some situations, but thankfully not in menus) functional.
|
||||
// We can either:
|
||||
// (a) Explicitly only set L2/R2/Digipad to 0 (and destroy their `ButtonPressed` field) => Needs to be documented, or
|
||||
// (b) ignore it as so far it seems only a 'visual' error
|
||||
// (L2/R2 being held down activates CrossHotBar but activating an ability is impossible because of the others blocked input,
|
||||
// Digipad is ignored in menus but without any menu's one still switches target or party members, but cannot interact with them
|
||||
// because of the other blocked input)
|
||||
// `ButtonPressed` is pretty useful so we opt-in to (b).
|
||||
// This is debatable.
|
||||
// ImGui itself does not care either way as it uses the Raw values and does its own state handling.
|
||||
// input->ButtonsRaw &= (ushort)~GamepadButtons.L2;
|
||||
// input->ButtonsRaw &= (ushort)~GamepadButtons.R2;
|
||||
// input->ButtonsRaw &= (ushort)~GamepadButtons.DpadDown;
|
||||
// input->ButtonsRaw &= (ushort)~GamepadButtons.DpadLeft;
|
||||
// input->ButtonsRaw &= (ushort)~GamepadButtons.DpadUp;
|
||||
// input->ButtonsRaw &= (ushort)~GamepadButtons.DpadRight;
|
||||
input->ButtonsPressed = 0;
|
||||
input->ButtonsReleased = 0;
|
||||
input->ButtonsRepeat = 0;
|
||||
return 0;
|
||||
if (this.NavEnableGamepad)
|
||||
{
|
||||
input->LeftStickX = 0;
|
||||
input->LeftStickY = 0;
|
||||
input->RightStickX = 0;
|
||||
input->RightStickY = 0;
|
||||
|
||||
// NOTE (Chiv) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased`
|
||||
// and `ButtonRepeat` as the game uses the RAW input to determine those (apparently).
|
||||
// It does block, however, all input to the game.
|
||||
// Leaving `ButtonsRaw` as it is and only zeroing the other leaves e.g. long-hold L2/R2
|
||||
// and the digipad (in some situations, but thankfully not in menus) functional.
|
||||
// We can either:
|
||||
// (a) Explicitly only set L2/R2/Digipad to 0 (and destroy their `ButtonPressed` field) => Needs to be documented, or
|
||||
// (b) ignore it as so far it seems only a 'visual' error
|
||||
// (L2/R2 being held down activates CrossHotBar but activating an ability is impossible because of the others blocked input,
|
||||
// Digipad is ignored in menus but without any menu's one still switches target or party members, but cannot interact with them
|
||||
// because of the other blocked input)
|
||||
// `ButtonPressed` is pretty useful but its hella confusing to the user, so we do (a) and advise plugins do not rely on
|
||||
// `ButtonPressed` while ImGuiConfigFlags.NavEnableGamepad is set.
|
||||
// This is debatable.
|
||||
// ImGui itself does not care either way as it uses the Raw values and does its own state handling.
|
||||
input->ButtonsRaw &= (ushort)~GamepadButtons.L2;
|
||||
input->ButtonsRaw &= (ushort)~GamepadButtons.R2;
|
||||
input->ButtonsRaw &= (ushort)~GamepadButtons.DpadDown;
|
||||
input->ButtonsRaw &= (ushort)~GamepadButtons.DpadLeft;
|
||||
input->ButtonsRaw &= (ushort)~GamepadButtons.DpadUp;
|
||||
input->ButtonsRaw &= (ushort)~GamepadButtons.DpadRight;
|
||||
input->ButtonsPressed = 0;
|
||||
input->ButtonsReleased = 0;
|
||||
input->ButtonsRepeat = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// NOTE (Chiv) Not so sure about the return value, does not seem to matter if we return the
|
||||
// original, zero or do the work adjusting the bits.
|
||||
return original;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Gamepad Poll detour critical error! Gamepad navigation will not work!");
|
||||
|
||||
// NOTE (Chiv) Not so sure about the return value, does not seem to matter if we return the
|
||||
// original, zero or do the work adjusting the bits.
|
||||
return original;
|
||||
// NOTE (Chiv) Explicitly deactivate on error
|
||||
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableGamepad;
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
|
|
|
|||
|
|
@ -304,6 +304,9 @@ namespace Dalamud.Interface
|
|||
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.NavEnableSetMousePos;
|
||||
}
|
||||
|
||||
// NOTE (Chiv) Explicitly deactivate on dalamud boot
|
||||
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableGamepad;
|
||||
|
||||
ImGuiHelpers.MainViewport = ImGui.GetMainViewport();
|
||||
|
||||
Log.Information("[IM] Scene & ImGui setup OK!");
|
||||
|
|
@ -484,7 +487,7 @@ namespace Dalamud.Interface
|
|||
&& this.dalamud.ClientState.GamepadState.Pressed(GamepadButtons.L3) > 0)
|
||||
{
|
||||
ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad;
|
||||
this.dalamud.DalamudUi.TogglePluginInstaller();
|
||||
this.dalamud.ClientState.GamepadState.NavEnableGamepad ^= true;
|
||||
}
|
||||
|
||||
if (gamepadEnabled
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue