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> /// </summary>
public bool IsDisableViewport { get; set; } = true; 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> /// <summary>
/// Load a configuration from the provided path. /// Load a configuration from the provided path.
/// </summary> /// </summary>

View file

@ -98,6 +98,11 @@ namespace Dalamud.Game.ClientState
/// </summary> /// </summary>
public KeyState KeyState; public KeyState KeyState;
/// <summary>
/// Provides access to the button state of gamepad buttons in game.
/// </summary>
public GamepadState GamepadState;
/// <summary> /// <summary>
/// Provides access to client conditions/player state. Allows you to check if a player is in a duty, mounted, etc. /// Provides access to client conditions/player state. Allows you to check if a player is in a duty, mounted, etc.
/// </summary> /// </summary>
@ -131,6 +136,8 @@ namespace Dalamud.Game.ClientState
this.KeyState = new KeyState(Address, scanner.Module.BaseAddress); this.KeyState = new KeyState(Address, scanner.Module.BaseAddress);
this.GamepadState = new GamepadState(this.Address);
this.Condition = new Condition( Address ); this.Condition = new Condition( Address );
this.Targets = new Targets(dalamud, Address); this.Targets = new Targets(dalamud, Address);
@ -150,6 +157,7 @@ namespace Dalamud.Game.ClientState
} }
public void Enable() { public void Enable() {
this.GamepadState.Enable();
this.PartyList.Enable(); this.PartyList.Enable();
this.setupTerritoryTypeHook.Enable(); this.setupTerritoryTypeHook.Enable();
} }
@ -158,6 +166,7 @@ namespace Dalamud.Game.ClientState
this.PartyList.Dispose(); this.PartyList.Dispose();
this.setupTerritoryTypeHook.Dispose(); this.setupTerritoryTypeHook.Dispose();
this.Actors.Dispose(); this.Actors.Dispose();
this.GamepadState.Dispose();
this.dalamud.Framework.OnUpdateEvent -= FrameworkOnOnUpdateEvent; this.dalamud.Framework.OnUpdateEvent -= FrameworkOnOnUpdateEvent;
this.dalamud.NetworkHandlers.CfPop += NetworkHandlersOnCfPop; this.dalamud.NetworkHandlers.CfPop += NetworkHandlersOnCfPop;

View file

@ -17,6 +17,13 @@ namespace Dalamud.Game.ClientState
//public IntPtr SomeActorTableAccess { get; private set; } //public IntPtr SomeActorTableAccess { get; private set; }
//public IntPtr PartyListUpdate { 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; } public IntPtr ConditionFlags { get; private set; }
protected override void Setup64Bit(SigScanner sig) { 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"); 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); 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[] private string[] dataKinds = new[]
{ {
"ServerOpCode", "Address", "Actor Table", "Font Test", "Party List", "Plugin IPC", "Condition", "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; private bool drawActors = false;
@ -391,6 +391,60 @@ namespace Dalamud.Interface
} }
break; 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 else

View file

@ -35,6 +35,7 @@ namespace Dalamud.Interface
private readonly ComponentDemoWindow componentDemoWindow; private readonly ComponentDemoWindow componentDemoWindow;
private readonly ColorDemoWindow colorDemoWindow; private readonly ColorDemoWindow colorDemoWindow;
private readonly ScratchpadWindow scratchpadWindow; private readonly ScratchpadWindow scratchpadWindow;
private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow;
private readonly WindowSystem windowSystem = new WindowSystem("DalamudCore"); private readonly WindowSystem windowSystem = new WindowSystem("DalamudCore");
@ -116,6 +117,9 @@ namespace Dalamud.Interface
}; };
this.windowSystem.AddWindow(this.scratchpadWindow); this.windowSystem.AddWindow(this.scratchpadWindow);
this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow();
this.windowSystem.AddWindow(this.gamepadModeNotifierWindow);
Log.Information("[DUI] Windows added"); Log.Information("[DUI] Windows added");
if (dalamud.Configuration.LogOpenAtStartup) if (dalamud.Configuration.LogOpenAtStartup)
@ -553,5 +557,13 @@ namespace Dalamud.Interface
{ {
this.scratchpadWindow.IsOpen ^= true; 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.dalamud = dalamud;
this.Size = new Vector2(740, 500); this.Size = new Vector2(740, 550);
this.SizeCondition = ImGuiCond.FirstUseEver; this.SizeCondition = ImGuiCond.FirstUseEver;
this.dalamudMessagesChatType = this.dalamud.Configuration.GeneralChatType; this.dalamudMessagesChatType = this.dalamud.Configuration.GeneralChatType;
@ -38,6 +38,7 @@ namespace Dalamud.Interface
this.doDocking = this.dalamud.Configuration.IsDocking; this.doDocking = this.dalamud.Configuration.IsDocking;
this.doViewport = !this.dalamud.Configuration.IsDisableViewport; this.doViewport = !this.dalamud.Configuration.IsDisableViewport;
this.doGamepad = this.dalamud.Configuration.IsGamepadNavigationEnabled;
this.doPluginTest = this.dalamud.Configuration.DoPluginTest; this.doPluginTest = this.dalamud.Configuration.DoPluginTest;
this.thirdRepoList = this.dalamud.Configuration.ThirdRepoList.Select(x => x.Clone()).ToList(); this.thirdRepoList = this.dalamud.Configuration.ThirdRepoList.Select(x => x.Clone()).ToList();
@ -133,6 +134,7 @@ namespace Dalamud.Interface
private bool doToggleUiHideDuringGpose; private bool doToggleUiHideDuringGpose;
private bool doDocking; private bool doDocking;
private bool doViewport; private bool doViewport;
private bool doGamepad;
private List<ThirdRepoSetting> thirdRepoList; private List<ThirdRepoSetting> thirdRepoList;
private bool printPluginsWelcomeMsg; private bool printPluginsWelcomeMsg;
@ -228,6 +230,9 @@ namespace Dalamud.Interface
ImGui.Checkbox(Loc.Localize("DalamudSettingToggleDocking", "Enable window docking"), ref this.doDocking); 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.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(); ImGui.EndTabItem();
} }
@ -378,6 +383,7 @@ namespace Dalamud.Interface
this.dalamud.Configuration.ToggleUiHideDuringGpose = this.doToggleUiHideDuringGpose; this.dalamud.Configuration.ToggleUiHideDuringGpose = this.doToggleUiHideDuringGpose;
this.dalamud.Configuration.IsDocking = this.doDocking; this.dalamud.Configuration.IsDocking = this.doDocking;
this.dalamud.Configuration.IsGamepadNavigationEnabled = this.doGamepad;
// This is applied every frame in InterfaceManager::CheckViewportState() // This is applied every frame in InterfaceManager::CheckViewportState()
this.dalamud.Configuration.IsDisableViewport = !this.doViewport; this.dalamud.Configuration.IsDisableViewport = !this.doViewport;
@ -392,6 +398,18 @@ namespace Dalamud.Interface
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable; 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.DoPluginTest = this.doPluginTest;
this.dalamud.Configuration.ThirdRepoList = this.thirdRepoList.Select(x => x.Clone()).ToList(); 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.Text;
using System.Threading; using System.Threading;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Internal.DXGI; using Dalamud.Game.Internal.DXGI;
using Dalamud.Hooking; using Dalamud.Hooking;
using EasyHook; using EasyHook;
@ -291,6 +292,21 @@ namespace Dalamud.Interface
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable; 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(); ImGuiHelpers.MainViewport = ImGui.GetMainViewport();
Log.Information("[IM] Scene & ImGui setup OK!"); Log.Information("[IM] Scene & ImGui setup OK!");
@ -461,6 +477,45 @@ namespace Dalamud.Interface
} }
// TODO: mouse state? // 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() private void Display()