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