diff --git a/Dalamud/Configuration/DalamudConfiguration.cs b/Dalamud/Configuration/DalamudConfiguration.cs
index dc9ea65ed..46ef9f208 100644
--- a/Dalamud/Configuration/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/DalamudConfiguration.cs
@@ -122,6 +122,11 @@ namespace Dalamud.Configuration
///
public bool IsDisableViewport { get; set; } = true;
+ ///
+ /// Gets or sets a value indicating whether or not navigation via a gamepad should be globally enabled in ImGui.
+ ///
+ public bool IsGamepadNavigationEnabled { get; set; } = true;
+
///
/// Load a configuration from the provided path.
///
diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs
index 9be2d194c..f5215cb1c 100644
--- a/Dalamud/Game/ClientState/ClientState.cs
+++ b/Dalamud/Game/ClientState/ClientState.cs
@@ -98,6 +98,11 @@ namespace Dalamud.Game.ClientState
///
public KeyState KeyState;
+ ///
+ /// Provides access to the button state of gamepad buttons in game.
+ ///
+ public GamepadState GamepadState;
+
///
/// Provides access to client conditions/player state. Allows you to check if a player is in a duty, mounted, etc.
///
@@ -131,6 +136,8 @@ namespace Dalamud.Game.ClientState
this.KeyState = new KeyState(Address, scanner.Module.BaseAddress);
+ this.GamepadState = new GamepadState(this.Address);
+
this.Condition = new Condition( Address );
this.Targets = new Targets(dalamud, Address);
@@ -150,6 +157,7 @@ namespace Dalamud.Game.ClientState
}
public void Enable() {
+ this.GamepadState.Enable();
this.PartyList.Enable();
this.setupTerritoryTypeHook.Enable();
}
@@ -158,6 +166,7 @@ namespace Dalamud.Game.ClientState
this.PartyList.Dispose();
this.setupTerritoryTypeHook.Dispose();
this.Actors.Dispose();
+ this.GamepadState.Dispose();
this.dalamud.Framework.OnUpdateEvent -= FrameworkOnOnUpdateEvent;
this.dalamud.NetworkHandlers.CfPop += NetworkHandlersOnCfPop;
diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
index adede5248..94591bf4e 100644
--- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
+++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
@@ -16,7 +16,14 @@ namespace Dalamud.Game.ClientState
public IntPtr SetupTerritoryType { get; private set; }
//public IntPtr SomeActorTableAccess { get; private set; }
//public IntPtr PartyListUpdate { get; private set; }
-
+
+ ///
+ /// Game function which polls the gamepads for data.
+ ///
+ /// Called every frame, even when `Enable Gamepad` is off in the settings.
+ ///
+ public IntPtr GamepadPoll { get; private set; }
+
public IntPtr ConditionFlags { get; private set; }
protected override void Setup64Bit(SigScanner sig) {
@@ -38,6 +45,8 @@ namespace Dalamud.Game.ClientState
ConditionFlags = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? BA ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 01 48 83 C4 30");
TargetManager = sig.GetStaticAddressFromSig("48 8B 05 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? FF 50 ?? 48 85 DB", 3);
+
+ this.GamepadPoll = sig.ScanText("40 ?? 57 41 ?? 48 81 EC ?? ?? ?? ?? 44 0F ?? ?? ?? ?? ?? ?? ?? 48 8B");
}
}
}
diff --git a/Dalamud/Game/ClientState/GamepadButtons.cs b/Dalamud/Game/ClientState/GamepadButtons.cs
new file mode 100644
index 000000000..80a745d53
--- /dev/null
+++ b/Dalamud/Game/ClientState/GamepadButtons.cs
@@ -0,0 +1,96 @@
+using System;
+
+namespace Dalamud.Game.ClientState
+{
+ ///
+ /// Bitmask of the Button ushort used by the game.
+ ///
+ [Flags]
+ public enum GamepadButtons : ushort
+ {
+ ///
+ /// No buttons pressed.
+ ///
+ None = 0,
+
+ ///
+ /// Digipad up.
+ ///
+ DpadUp = 0x0001,
+
+ ///
+ /// Digipad down.
+ ///
+ DpadDown = 0x0002,
+
+ ///
+ /// Digipad left.
+ ///
+ DpadLeft = 0x0004,
+
+ ///
+ /// Digipad right.
+ ///
+ DpadRight = 0x0008,
+
+ ///
+ /// North action button. Triangle on PS, Y on Xbox.
+ ///
+ North = 0x0010,
+
+ ///
+ /// South action button. Cross on PS, A on Xbox.
+ ///
+ South = 0x0020,
+
+ ///
+ /// West action button. Square on PS, X on Xbos.
+ ///
+ West = 0x0040,
+
+ ///
+ /// East action button. Circle on PS, B on Xbox.
+ ///
+ East = 0x0080,
+
+ ///
+ /// First button on left shoulder side.
+ ///
+ L1 = 0x0100,
+
+ ///
+ /// Second button on left shoulder side. Analog input lost in this bitmask.
+ ///
+ L2 = 0x0200,
+
+ ///
+ /// Press on left analogue stick.
+ ///
+ L3 = 0x0400,
+
+ ///
+ /// First button on right shoulder.
+ ///
+ R1 = 0x0800,
+
+ ///
+ /// Second button on right shoulder. Analog input lost in this bitmask.
+ ///
+ R2 = 0x1000,
+
+ ///
+ /// Press on right analogue stick.
+ ///
+ R3 = 0x2000,
+
+ ///
+ /// Button on the right inner side of the controller. Options on PS, Start on Xbox.
+ ///
+ Start = 0x8000,
+
+ ///
+ /// Button on the left inner side of the controller. ??? on PS, Back on Xbox.
+ ///
+ Select = 0x4000,
+ }
+}
diff --git a/Dalamud/Game/ClientState/GamepadState.cs b/Dalamud/Game/ClientState/GamepadState.cs
new file mode 100644
index 000000000..b9711d093
--- /dev/null
+++ b/Dalamud/Game/ClientState/GamepadState.cs
@@ -0,0 +1,270 @@
+using System;
+
+using Dalamud.Game.ClientState.Structs;
+using Dalamud.Hooking;
+using ImGuiNET;
+using Serilog;
+
+namespace Dalamud.Game.ClientState
+{
+ ///
+ /// Exposes the game gamepad state to dalamud.
+ ///
+ /// Will block game's gamepad input if is set.
+ ///
+ public unsafe class GamepadState
+ {
+ private readonly Hook gamepadPoll;
+
+ private bool isDisposed;
+
+ private int leftStickX;
+ private int leftStickY;
+ private int rightStickX;
+ private int rightStickY;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Resolver knowing the pointer to the GamepadPoll function.
+ public GamepadState(ClientStateAddressResolver resolver)
+ {
+#if DEBUG
+ Log.Verbose("GamepadPoll address {GamepadPoll}", resolver.GamepadPoll);
+#endif
+ this.gamepadPoll = new Hook(
+ resolver.GamepadPoll,
+ (ControllerPoll)this.GamepadPollDetour);
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~GamepadState()
+ {
+ this.Dispose(false);
+ }
+
+ private delegate int ControllerPoll(IntPtr controllerInput);
+
+#if DEBUG
+ ///
+ /// Gets the pointer to the current instance of the GamepadInput struct.
+ ///
+ public IntPtr GamepadInput { get; private set; }
+#endif
+
+ ///
+ /// Gets the state of the left analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
+ ///
+ public float LeftStickLeft => this.leftStickX < 0 ? -this.leftStickX / 100f : 0;
+
+ ///
+ /// Gets the state of the left analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
+ ///
+ public float LeftStickRight => this.leftStickX > 0 ? this.leftStickX / 100f : 0;
+
+ ///
+ /// Gets the state of the left analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
+ ///
+ public float LeftStickUp => this.leftStickY > 0 ? this.leftStickY / 100f : 0;
+
+ ///
+ /// Gets the state of the left analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
+ ///
+ public float LeftStickDown => this.leftStickY < 0 ? -this.leftStickY / 100f : 0;
+
+ ///
+ /// Gets the state of the right analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
+ ///
+ public float RightStickLeft => this.rightStickX < 0 ? -this.rightStickX / 100f : 0;
+
+ ///
+ /// Gets the state of the right analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
+ ///
+ public float RightStickRight => this.rightStickX > 0 ? this.rightStickX / 100f : 0;
+
+ ///
+ /// Gets the state of the right analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
+ ///
+ public float RightStickUp => this.rightStickY > 0 ? this.rightStickY / 100f : 0;
+
+ ///
+ /// Gets the state of the right analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
+ ///
+ public float RightStickDown => this.rightStickY < 0 ? -this.rightStickY / 100f : 0;
+
+ ///
+ /// Gets buttons pressed bitmask, set once when the button is pressed. See for the mapping.
+ ///
+ /// Exposed internally for Debug Data window.
+ ///
+ internal ushort ButtonsPressed { get; private set; }
+
+ ///
+ /// Gets raw button bitmask, set the whole time while a button is held. See for the mapping.
+ ///
+ /// Exposed internally for Debug Data window.
+ ///
+ internal ushort ButtonsRaw { get; private set; }
+
+ ///
+ /// Gets button released bitmask, set once right after the button is not hold anymore. See for the mapping.
+ ///
+ /// Exposed internally for Debug Data window.
+ ///
+ internal ushort ButtonsReleased { get; private set; }
+
+ ///
+ /// Gets button repeat bitmask, emits the held button input in fixed intervals. See for the mapping.
+ ///
+ /// Exposed internally for Debug Data window.
+ ///
+ internal ushort ButtonsRepeat { get; private set; }
+
+ ///
+ /// 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.
+ ///
+ internal bool NavEnableGamepad { get; set; }
+
+ ///
+ /// Gets whether has been pressed.
+ ///
+ /// Only true on first frame of the press.
+ /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
+ ///
+ /// The button to check for.
+ /// 1 if pressed, 0 otherwise.
+ public float Pressed(GamepadButtons button) => (this.ButtonsPressed & (ushort)button) > 0 ? 1 : 0;
+
+ ///
+ /// Gets whether is being pressed.
+ ///
+ /// True in intervals if button is held down.
+ /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
+ ///
+ /// The button to check for.
+ /// 1 if still pressed during interval, 0 otherwise or in between intervals.
+ public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & (ushort)button) > 0 ? 1 : 0;
+
+ ///
+ /// Gets whether has been released.
+ ///
+ /// Only true the frame after release.
+ /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
+ ///
+ /// The button to check for.
+ /// 1 if released, 0 otherwise.
+ public float Released(GamepadButtons button) => (this.ButtonsReleased & (ushort)button) > 0 ? 1 : 0;
+
+ ///
+ /// Gets the raw state of .
+ ///
+ /// Is set the entire time a button is pressed down.
+ ///
+ /// The button to check for.
+ /// 1 the whole time button is pressed, 0 otherwise.
+ public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0;
+
+ ///
+ /// Enables the hook of the GamepadPoll function.
+ ///
+ public void Enable()
+ {
+ this.gamepadPoll.Enable();
+ }
+
+ ///
+ /// Disposes this instance, alongside its hooks.
+ ///
+ public void Dispose()
+ {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private int GamepadPollDetour(IntPtr gamepadInput)
+ {
+ var original = this.gamepadPoll.Original(gamepadInput);
+ try
+ {
+#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;
+
+ 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.
+ const ushort deletionMask = (ushort)(~GamepadButtons.L2
+ & ~GamepadButtons.R2
+ & ~GamepadButtons.DpadDown
+ & ~GamepadButtons.DpadLeft
+ & ~GamepadButtons.DpadUp
+ & ~GamepadButtons.DpadRight);
+ input->ButtonsRaw &= deletionMask;
+ 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) Explicitly deactivate on error
+ ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableGamepad;
+ return original;
+ }
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (this.isDisposed) return;
+ if (disposing)
+ {
+ this.gamepadPoll?.Disable();
+ this.gamepadPoll?.Dispose();
+ }
+
+ this.isDisposed = true;
+ }
+ }
+}
diff --git a/Dalamud/Game/ClientState/Structs/GamepadInput.cs b/Dalamud/Game/ClientState/Structs/GamepadInput.cs
new file mode 100644
index 000000000..ce7440b61
--- /dev/null
+++ b/Dalamud/Game/ClientState/Structs/GamepadInput.cs
@@ -0,0 +1,64 @@
+using System.Runtime.InteropServices;
+
+namespace Dalamud.Game.ClientState.Structs
+{
+ ///
+ /// Struct which gets populated by polling the gamepads.
+ ///
+ /// Has an array of gamepads, among many other things (here not mapped).
+ /// All we really care about is the final data which the game uses to determine input.
+ ///
+ /// The size is definitely bigger than only the following fields but I do not know how big.
+ ///
+ [StructLayout(LayoutKind.Explicit)]
+ public struct GamepadInput
+ {
+ ///
+ /// Left analogue stick's horizontal value, -99 for left, 99 for right.
+ ///
+ [FieldOffset(0x88)]
+ public int LeftStickX;
+
+ ///
+ /// Left analogue stick's vertical value, -99 for down, 99 for up.
+ ///
+ [FieldOffset(0x8C)]
+ public int LeftStickY;
+
+ ///
+ /// Right analogue stick's horizontal value, -99 for left, 99 for right.
+ ///
+ [FieldOffset(0x90)]
+ public int RightStickX;
+
+ ///
+ /// Right analogue stick's vertical value, -99 for down, 99 for up.
+ ///
+ [FieldOffset(0x94)]
+ public int RightStickY;
+
+ ///
+ /// Raw input, set the whole time while a button is held. See for the mapping.
+ ///
+ [FieldOffset(0x98)]
+ public ushort ButtonsRaw; // bitfield
+
+ ///
+ /// Button pressed, set once when the button is pressed. See for the mapping.
+ ///
+ [FieldOffset(0x9C)]
+ public ushort ButtonsPressed; // bitfield
+
+ ///
+ /// Button released input, set once right after the button is not hold anymore. See for the mapping.
+ ///
+ [FieldOffset(0xA0)]
+ public ushort ButtonsReleased; // bitfield
+
+ ///
+ /// Repeatedly emits the held button input in fixed intervals. See for the mapping.
+ ///
+ [FieldOffset(0xA4)]
+ public ushort ButtonsRepeat; // bitfield
+ }
+}
diff --git a/Dalamud/Interface/DalamudDataWindow.cs b/Dalamud/Interface/DalamudDataWindow.cs
index 5285a3a74..460132394 100644
--- a/Dalamud/Interface/DalamudDataWindow.cs
+++ b/Dalamud/Interface/DalamudDataWindow.cs
@@ -36,7 +36,7 @@ namespace Dalamud.Interface
private string[] dataKinds = new[]
{
"ServerOpCode", "Address", "Actor Table", "Font Test", "Party List", "Plugin IPC", "Condition",
- "Gauge", "Command", "Addon", "Addon Inspector", "StartInfo", "Target", "Toast", "ImGui", "Tex",
+ "Gauge", "Command", "Addon", "Addon Inspector", "StartInfo", "Target", "Toast", "ImGui", "Tex", "Gamepad",
};
private bool drawActors = false;
@@ -391,6 +391,60 @@ namespace Dalamud.Interface
}
break;
+
+ // Gamepad
+ case 16:
+ Action> helper = (text, mask, resolve) =>
+ {
+ ImGui.Text($"{text} {mask:X4}");
+ ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " +
+ $"DPadUp {resolve(GamepadButtons.DpadUp)} " +
+ $"DPadRight {resolve(GamepadButtons.DpadRight)} " +
+ $"DPadDown {resolve(GamepadButtons.DpadDown)} ");
+ ImGui.Text($"West {resolve(GamepadButtons.West)} " +
+ $"North {resolve(GamepadButtons.North)} " +
+ $"East {resolve(GamepadButtons.East)} " +
+ $"South {resolve(GamepadButtons.South)} ");
+ ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " +
+ $"L2 {resolve(GamepadButtons.L2)} " +
+ $"R1 {resolve(GamepadButtons.R1)} " +
+ $"R2 {resolve(GamepadButtons.R2)} ");
+ ImGui.Text($"Select {resolve(GamepadButtons.Select)} " +
+ $"Start {resolve(GamepadButtons.Start)} " +
+ $"L3 {resolve(GamepadButtons.L3)} " +
+ $"R3 {resolve(GamepadButtons.R3)} ");
+ };
+#if DEBUG
+ ImGui.Text($"GamepadInput {this.dalamud.ClientState.GamepadState.GamepadInput.ToString("X")}");
+ if (ImGui.IsItemHovered()) ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
+ if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{this.dalamud.ClientState.GamepadState.GamepadInput.ToString("X")}");
+#endif
+
+ helper(
+ "Buttons Raw",
+ this.dalamud.ClientState.GamepadState.ButtonsRaw,
+ this.dalamud.ClientState.GamepadState.Raw);
+ helper(
+ "Buttons Pressed",
+ this.dalamud.ClientState.GamepadState.ButtonsPressed,
+ this.dalamud.ClientState.GamepadState.Pressed);
+ helper(
+ "Buttons Repeat",
+ this.dalamud.ClientState.GamepadState.ButtonsRepeat,
+ this.dalamud.ClientState.GamepadState.Repeat);
+ helper(
+ "Buttons Released",
+ this.dalamud.ClientState.GamepadState.ButtonsReleased,
+ this.dalamud.ClientState.GamepadState.Released);
+ ImGui.Text($"LeftStickLeft {this.dalamud.ClientState.GamepadState.LeftStickLeft:0.00} " +
+ $"LeftStickUp {this.dalamud.ClientState.GamepadState.LeftStickUp:0.00} " +
+ $"LeftStickRight {this.dalamud.ClientState.GamepadState.LeftStickRight:0.00} " +
+ $"LeftStickDown {this.dalamud.ClientState.GamepadState.LeftStickDown:0.00} ");
+ ImGui.Text($"RightStickLeft {this.dalamud.ClientState.GamepadState.RightStickLeft:0.00} " +
+ $"RightStickUp {this.dalamud.ClientState.GamepadState.RightStickUp:0.00} " +
+ $"RightStickRight {this.dalamud.ClientState.GamepadState.RightStickRight:0.00} " +
+ $"RightStickDown {this.dalamud.ClientState.GamepadState.RightStickDown:0.00} ");
+ break;
}
}
else
diff --git a/Dalamud/Interface/DalamudInterface.cs b/Dalamud/Interface/DalamudInterface.cs
index 9283ff79e..e7d330b55 100644
--- a/Dalamud/Interface/DalamudInterface.cs
+++ b/Dalamud/Interface/DalamudInterface.cs
@@ -35,6 +35,7 @@ namespace Dalamud.Interface
private readonly ComponentDemoWindow componentDemoWindow;
private readonly ColorDemoWindow colorDemoWindow;
private readonly ScratchpadWindow scratchpadWindow;
+ private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow;
private readonly WindowSystem windowSystem = new WindowSystem("DalamudCore");
@@ -116,6 +117,9 @@ namespace Dalamud.Interface
};
this.windowSystem.AddWindow(this.scratchpadWindow);
+ this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow();
+ this.windowSystem.AddWindow(this.gamepadModeNotifierWindow);
+
Log.Information("[DUI] Windows added");
if (dalamud.Configuration.LogOpenAtStartup)
@@ -553,5 +557,13 @@ namespace Dalamud.Interface
{
this.scratchpadWindow.IsOpen ^= true;
}
+
+ ///
+ /// Toggle the gamepad notifier window window.
+ ///
+ internal void ToggleGamePadNotifierWindow()
+ {
+ this.gamepadModeNotifierWindow.IsOpen ^= true;
+ }
}
}
diff --git a/Dalamud/Interface/DalamudSettingsWindow.cs b/Dalamud/Interface/DalamudSettingsWindow.cs
index 152e7ff32..8cc68ec80 100644
--- a/Dalamud/Interface/DalamudSettingsWindow.cs
+++ b/Dalamud/Interface/DalamudSettingsWindow.cs
@@ -23,7 +23,7 @@ namespace Dalamud.Interface
{
this.dalamud = dalamud;
- this.Size = new Vector2(740, 500);
+ this.Size = new Vector2(740, 550);
this.SizeCondition = ImGuiCond.FirstUseEver;
this.dalamudMessagesChatType = this.dalamud.Configuration.GeneralChatType;
@@ -38,6 +38,7 @@ namespace Dalamud.Interface
this.doDocking = this.dalamud.Configuration.IsDocking;
this.doViewport = !this.dalamud.Configuration.IsDisableViewport;
+ this.doGamepad = this.dalamud.Configuration.IsGamepadNavigationEnabled;
this.doPluginTest = this.dalamud.Configuration.DoPluginTest;
this.thirdRepoList = this.dalamud.Configuration.ThirdRepoList.Select(x => x.Clone()).ToList();
@@ -133,6 +134,7 @@ namespace Dalamud.Interface
private bool doToggleUiHideDuringGpose;
private bool doDocking;
private bool doViewport;
+ private bool doGamepad;
private List thirdRepoList;
private bool printPluginsWelcomeMsg;
@@ -228,6 +230,9 @@ namespace Dalamud.Interface
ImGui.Checkbox(Loc.Localize("DalamudSettingToggleDocking", "Enable window docking"), ref this.doDocking);
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingToggleDockingHint", "This will allow you to fuse and tab plugin windows."));
+ ImGui.Checkbox(Loc.Localize("DalamudSettingToggleGamepadNavigation", "Enable navigation of ImGui windows via gamepad."), ref this.doGamepad);
+ ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingToggleGamepadNavigationHint", "This will allow you to toggle between game and ImGui navigation via L1+L3.\nToggle the PluginInstaller window via R3 if ImGui navigation is enabled."));
+
ImGui.EndTabItem();
}
@@ -378,6 +383,7 @@ namespace Dalamud.Interface
this.dalamud.Configuration.ToggleUiHideDuringGpose = this.doToggleUiHideDuringGpose;
this.dalamud.Configuration.IsDocking = this.doDocking;
+ this.dalamud.Configuration.IsGamepadNavigationEnabled = this.doGamepad;
// This is applied every frame in InterfaceManager::CheckViewportState()
this.dalamud.Configuration.IsDisableViewport = !this.doViewport;
@@ -392,6 +398,18 @@ namespace Dalamud.Interface
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable;
}
+ // NOTE (Chiv) Toggle gamepad navigation via setting
+ if (!this.dalamud.Configuration.IsGamepadNavigationEnabled)
+ {
+ ImGui.GetIO().BackendFlags &= ~ImGuiBackendFlags.HasGamepad;
+ ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableSetMousePos;
+ }
+ else
+ {
+ ImGui.GetIO().BackendFlags |= ImGuiBackendFlags.HasGamepad;
+ ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.NavEnableSetMousePos;
+ }
+
this.dalamud.Configuration.DoPluginTest = this.doPluginTest;
this.dalamud.Configuration.ThirdRepoList = this.thirdRepoList.Select(x => x.Clone()).ToList();
diff --git a/Dalamud/Interface/GamepadModeNotifierWindow.cs b/Dalamud/Interface/GamepadModeNotifierWindow.cs
new file mode 100644
index 000000000..1bcf63e6e
--- /dev/null
+++ b/Dalamud/Interface/GamepadModeNotifierWindow.cs
@@ -0,0 +1,47 @@
+using System.Numerics;
+
+using CheapLoc;
+using Dalamud.Interface.Windowing;
+using ImGuiNET;
+
+namespace Dalamud.Interface
+{
+ ///
+ /// Class responsible for drawing a notifier on screen that gamepad mode is active.
+ ///
+ internal class GamepadModeNotifierWindow : Window
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public GamepadModeNotifierWindow()
+ : base(
+ "###DalamudGamepadModeNotifier",
+ ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoMouseInputs
+ | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoNav
+ | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoSavedSettings,
+ true)
+ {
+ this.Size = Vector2.Zero;
+ this.SizeCondition = ImGuiCond.Always;
+ this.IsOpen = false;
+ }
+
+ ///
+ /// Draws a light grey-ish, main-viewport-big filled rect in the background draw list alongside a text indicating gamepad mode.
+ ///
+ public override void Draw()
+ {
+ var drawList = ImGui.GetBackgroundDrawList();
+ drawList.PushClipRectFullScreen();
+ drawList.AddRectFilled(Vector2.Zero, ImGuiHelpers.MainViewport.Size, 0x661A1A1A);
+ drawList.AddText(
+ Vector2.One,
+ 0xFFFFFFFF,
+ Loc.Localize(
+ "DalamudGamepadModeNotifierText",
+ "Gamepad mode is ON. Press R1+L3 to deactivate, press R3 to toggle PluginInstaller."));
+ drawList.PopClipRect();
+ }
+ }
+}
diff --git a/Dalamud/Interface/InterfaceManager.cs b/Dalamud/Interface/InterfaceManager.cs
index 854fc9dbe..4d7f8ecc4 100644
--- a/Dalamud/Interface/InterfaceManager.cs
+++ b/Dalamud/Interface/InterfaceManager.cs
@@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Dalamud.Game;
+using Dalamud.Game.ClientState;
using Dalamud.Game.Internal.DXGI;
using Dalamud.Hooking;
using EasyHook;
@@ -291,6 +292,21 @@ namespace Dalamud.Interface
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable;
}
+ // NOTE (Chiv) Toggle gamepad navigation via setting
+ if (!this.dalamud.Configuration.IsGamepadNavigationEnabled)
+ {
+ ImGui.GetIO().BackendFlags &= ~ImGuiBackendFlags.HasGamepad;
+ ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableSetMousePos;
+ }
+ else
+ {
+ ImGui.GetIO().BackendFlags |= ImGuiBackendFlags.HasGamepad;
+ 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!");
@@ -461,6 +477,45 @@ namespace Dalamud.Interface
}
// TODO: mouse state?
+
+ var gamepadEnabled = (ImGui.GetIO().BackendFlags & ImGuiBackendFlags.HasGamepad) > 0;
+
+ // NOTE (Chiv) Activate ImGui navigation via L1+L3 press
+ // (mimicking how mouse navigation is activated via L1+R3 press in game).
+ if (gamepadEnabled
+ && this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.L1) > 0
+ && this.dalamud.ClientState.GamepadState.Pressed(GamepadButtons.L3) > 0)
+ {
+ ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad;
+ this.dalamud.ClientState.GamepadState.NavEnableGamepad ^= true;
+ this.dalamud.DalamudUi.ToggleGamePadNotifierWindow();
+ }
+
+ if (gamepadEnabled
+ && (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0)
+ {
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.Activate] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.South);
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.Cancel] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.East);
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.Input] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.North);
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.Menu] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.West);
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.DpadLeft] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.DpadLeft);
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.DpadRight] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.DpadRight);
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.DpadUp] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.DpadUp);
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.DpadDown] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.DpadDown);
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.LStickLeft] = this.dalamud.ClientState.GamepadState.LeftStickLeft;
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.LStickRight] = this.dalamud.ClientState.GamepadState.LeftStickRight;
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.LStickUp] = this.dalamud.ClientState.GamepadState.LeftStickUp;
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.LStickDown] = this.dalamud.ClientState.GamepadState.LeftStickDown;
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.FocusPrev] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.L1);
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.FocusNext] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.R1);
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.TweakSlow] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.L2);
+ ImGui.GetIO().NavInputs[(int)ImGuiNavInput.TweakFast] = this.dalamud.ClientState.GamepadState.Raw(GamepadButtons.R2);
+
+ if (this.dalamud.ClientState.GamepadState.Pressed(GamepadButtons.R3) > 0)
+ {
+ this.dalamud.DalamudUi.TogglePluginInstaller();
+ }
+ }
}
private void Display()