Merge pull request #328 from fitzchivalrik/feature/ControllerNavigation

This commit is contained in:
goaaats 2021-05-02 18:16:38 +02:00 committed by GitHub
commit 2a79ac58e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 642 additions and 3 deletions

View file

@ -122,6 +122,11 @@ namespace Dalamud.Configuration
/// </summary>
public bool IsDisableViewport { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not navigation via a gamepad should be globally enabled in ImGui.
/// </summary>
public bool IsGamepadNavigationEnabled { get; set; } = true;
/// <summary>
/// Load a configuration from the provided path.
/// </summary>

View file

@ -98,6 +98,11 @@ namespace Dalamud.Game.ClientState
/// </summary>
public KeyState KeyState;
/// <summary>
/// Provides access to the button state of gamepad buttons in game.
/// </summary>
public GamepadState GamepadState;
/// <summary>
/// Provides access to client conditions/player state. Allows you to check if a player is in a duty, mounted, etc.
/// </summary>
@ -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;

View file

@ -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; }
/// <summary>
/// Game function which polls the gamepads for data.
///
/// Called every frame, even when `Enable Gamepad` is off in the settings.
/// </summary>
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");
}
}
}

View file

@ -0,0 +1,96 @@
using System;
namespace Dalamud.Game.ClientState
{
/// <summary>
/// Bitmask of the Button ushort used by the game.
/// </summary>
[Flags]
public enum GamepadButtons : ushort
{
/// <summary>
/// No buttons pressed.
/// </summary>
None = 0,
/// <summary>
/// Digipad up.
/// </summary>
DpadUp = 0x0001,
/// <summary>
/// Digipad down.
/// </summary>
DpadDown = 0x0002,
/// <summary>
/// Digipad left.
/// </summary>
DpadLeft = 0x0004,
/// <summary>
/// Digipad right.
/// </summary>
DpadRight = 0x0008,
/// <summary>
/// North action button. Triangle on PS, Y on Xbox.
/// </summary>
North = 0x0010,
/// <summary>
/// South action button. Cross on PS, A on Xbox.
/// </summary>
South = 0x0020,
/// <summary>
/// West action button. Square on PS, X on Xbos.
/// </summary>
West = 0x0040,
/// <summary>
/// East action button. Circle on PS, B on Xbox.
/// </summary>
East = 0x0080,
/// <summary>
/// First button on left shoulder side.
/// </summary>
L1 = 0x0100,
/// <summary>
/// Second button on left shoulder side. Analog input lost in this bitmask.
/// </summary>
L2 = 0x0200,
/// <summary>
/// Press on left analogue stick.
/// </summary>
L3 = 0x0400,
/// <summary>
/// First button on right shoulder.
/// </summary>
R1 = 0x0800,
/// <summary>
/// Second button on right shoulder. Analog input lost in this bitmask.
/// </summary>
R2 = 0x1000,
/// <summary>
/// Press on right analogue stick.
/// </summary>
R3 = 0x2000,
/// <summary>
/// Button on the right inner side of the controller. Options on PS, Start on Xbox.
/// </summary>
Start = 0x8000,
/// <summary>
/// Button on the left inner side of the controller. ??? on PS, Back on Xbox.
/// </summary>
Select = 0x4000,
}
}

View file

@ -0,0 +1,270 @@
using System;
using Dalamud.Game.ClientState.Structs;
using Dalamud.Hooking;
using ImGuiNET;
using Serilog;
namespace Dalamud.Game.ClientState
{
/// <summary>
/// Exposes the game gamepad state to dalamud.
///
/// Will block game's gamepad input if <see cref="ImGuiConfigFlags.NavEnableGamepad"/> is set.
/// </summary>
public unsafe class GamepadState
{
private readonly Hook<ControllerPoll> gamepadPoll;
private bool isDisposed;
private int leftStickX;
private int leftStickY;
private int rightStickX;
private int rightStickY;
/// <summary>
/// Initializes a new instance of the <see cref="GamepadState" /> class.
/// </summary>
/// <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);
}
/// <summary>
/// Finalizes an instance of the <see cref="GamepadState" /> class.
/// </summary>
~GamepadState()
{
this.Dispose(false);
}
private delegate int ControllerPoll(IntPtr controllerInput);
#if DEBUG
/// <summary>
/// Gets the pointer to the current instance of the GamepadInput struct.
/// </summary>
public IntPtr GamepadInput { get; private set; }
#endif
/// <summary>
/// Gets the state of the left analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickLeft => this.leftStickX < 0 ? -this.leftStickX / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickRight => this.leftStickX > 0 ? this.leftStickX / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickUp => this.leftStickY > 0 ? this.leftStickY / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickDown => this.leftStickY < 0 ? -this.leftStickY / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickLeft => this.rightStickX < 0 ? -this.rightStickX / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickRight => this.rightStickX > 0 ? this.rightStickX / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickUp => this.rightStickY > 0 ? this.rightStickY / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickDown => this.rightStickY < 0 ? -this.rightStickY / 100f : 0;
/// <summary>
/// Gets buttons pressed bitmask, set once when the button is pressed. See <see cref="GamepadButtons"/> for the mapping.
///
/// Exposed internally for Debug Data window.
/// </summary>
internal ushort ButtonsPressed { get; private set; }
/// <summary>
/// Gets raw button bitmask, set the whole time while a button is held. See <see cref="GamepadButtons"/> for the mapping.
///
/// Exposed internally for Debug Data window.
/// </summary>
internal ushort ButtonsRaw { get; private set; }
/// <summary>
/// Gets button released bitmask, set once right after the button is not hold anymore. See <see cref="GamepadButtons"/> for the mapping.
///
/// Exposed internally for Debug Data window.
/// </summary>
internal ushort ButtonsReleased { get; private set; }
/// <summary>
/// Gets button repeat bitmask, emits the held button input in fixed intervals. See <see cref="GamepadButtons"/> for the mapping.
///
/// Exposed internally for Debug Data window.
/// </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>
public float Pressed(GamepadButtons button) => (this.ButtonsPressed & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// 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>
public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// 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>
public float Released(GamepadButtons button) => (this.ButtonsReleased & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Gets the raw state of <paramref name="button"/>.
///
/// Is set the entire time a button is pressed down.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 the whole time button is pressed, 0 otherwise.</returns>
public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Enables the hook of the GamepadPoll function.
/// </summary>
public void Enable()
{
this.gamepadPoll.Enable();
}
/// <summary>
/// Disposes this instance, alongside its hooks.
/// </summary>
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;
}
}
}

View file

@ -0,0 +1,64 @@
using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.Structs
{
/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct GamepadInput
{
/// <summary>
/// Left analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary>
[FieldOffset(0x88)]
public int LeftStickX;
/// <summary>
/// Left analogue stick's vertical value, -99 for down, 99 for up.
/// </summary>
[FieldOffset(0x8C)]
public int LeftStickY;
/// <summary>
/// Right analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary>
[FieldOffset(0x90)]
public int RightStickX;
/// <summary>
/// Right analogue stick's vertical value, -99 for down, 99 for up.
/// </summary>
[FieldOffset(0x94)]
public int RightStickY;
/// <summary>
/// Raw input, set the whole time while a button is held. See <see cref="GamepadButtons"/> for the mapping.
/// </summary>
[FieldOffset(0x98)]
public ushort ButtonsRaw; // bitfield
/// <summary>
/// Button pressed, set once when the button is pressed. See <see cref="GamepadButtons"/> for the mapping.
/// </summary>
[FieldOffset(0x9C)]
public ushort ButtonsPressed; // bitfield
/// <summary>
/// Button released input, set once right after the button is not hold anymore. See <see cref="GamepadButtons"/> for the mapping.
/// </summary>
[FieldOffset(0xA0)]
public ushort ButtonsReleased; // bitfield
/// <summary>
/// Repeatedly emits the held button input in fixed intervals. See <see cref="GamepadButtons"/> for the mapping.
/// </summary>
[FieldOffset(0xA4)]
public ushort ButtonsRepeat; // bitfield
}
}

View file

@ -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<string, uint, Func<GamepadButtons, float>> 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

View file

@ -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;
}
/// <summary>
/// Toggle the gamepad notifier window window.
/// </summary>
internal void ToggleGamePadNotifierWindow()
{
this.gamepadModeNotifierWindow.IsOpen ^= true;
}
}
}

View file

@ -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<ThirdRepoSetting> 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();

View file

@ -0,0 +1,47 @@
using System.Numerics;
using CheapLoc;
using Dalamud.Interface.Windowing;
using ImGuiNET;
namespace Dalamud.Interface
{
/// <summary>
/// Class responsible for drawing a notifier on screen that gamepad mode is active.
/// </summary>
internal class GamepadModeNotifierWindow : Window
{
/// <summary>
/// Initializes a new instance of the <see cref="GamepadModeNotifierWindow"/> class.
/// </summary>
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;
}
/// <summary>
/// Draws a light grey-ish, main-viewport-big filled rect in the background draw list alongside a text indicating gamepad mode.
/// </summary>
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();
}
}
}

View file

@ -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()