4365f78ef7
Co-authored-by: Cursor <cursoragent@cursor.com>
647 lines
26 KiB
C#
647 lines
26 KiB
C#
/*
|
|
Copyright(c) 2021 attickdoor (https://github.com/attickdoor/MOActionPlugin)
|
|
Modifications Copyright(c) 2021 HSUI
|
|
09/21/2021 - Used original's code hooks and action validations while using
|
|
HSUI's own logic to select a target.
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
using Dalamud.Game.ClientState.Objects.Enums;
|
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
using Dalamud.Game.ClientState.Objects.Types;
|
|
using Dalamud.Hooking;
|
|
using Dalamud.Plugin.Services;
|
|
using HSUI.Config;
|
|
using HSUI.Interface.GeneralElements;
|
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
using System.Runtime.CompilerServices;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Lumina.Excel;
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using static FFXIVClientStructs.FFXIV.Client.Game.ActionManager;
|
|
using Action = Lumina.Excel.Sheets.Action;
|
|
using BattleNpcSubKind = Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind;
|
|
|
|
namespace HSUI.Helpers
|
|
{
|
|
public unsafe class InputsHelper : IDisposable
|
|
{
|
|
private delegate bool UseActionDelegate(ActionManager* manager, ActionType actionType, uint actionId, ulong targetId, uint extraParam, UseActionMode mode, uint comboRouteId, bool* outOptAreaTargeted);
|
|
|
|
private delegate byte ExecuteSlotByIdDelegate(RaptureHotbarModule* module, uint hotbarId, uint slotId);
|
|
|
|
#region Singleton
|
|
private InputsHelper()
|
|
{
|
|
_sheet = Plugin.DataManager.GetExcelSheet<Action>();
|
|
|
|
//try
|
|
//{
|
|
// /*
|
|
// Part of setUIMouseOverActorId disassembly signature
|
|
// .text:00007FF64830FD70 sub_7FF64830FD70 proc near
|
|
// .text:00007FF64830FD70 48 89 91 90 02 00+mov [rcx+290h], rdx
|
|
// .text:00007FF64830FD70 00
|
|
// */
|
|
|
|
// _uiMouseOverActorHook = Plugin.GameInteropProvider.HookFromSignature<OnSetUIMouseoverActor>(
|
|
// "E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 4C 8B 74 24 ?? 83 FD 02",
|
|
// HandleUIMouseOverActorId
|
|
// );
|
|
//}
|
|
//catch
|
|
//{
|
|
// Plugin.Logger.Error("InputsHelper OnSetUIMouseoverActor Hook failed!!!");
|
|
//}
|
|
|
|
try
|
|
{
|
|
_requestActionHook = Plugin.GameInteropProvider.HookFromSignature<UseActionDelegate>(
|
|
ActionManager.Addresses.UseAction.String,
|
|
HandleRequestAction
|
|
);
|
|
_requestActionHook?.Enable();
|
|
}
|
|
catch
|
|
{
|
|
Plugin.Logger.Error("InputsHelper UseActionDelegate Hook failed!!!");
|
|
}
|
|
|
|
try
|
|
{
|
|
nint addr = (nint)RaptureHotbarModule.Addresses.ExecuteSlotById.Value;
|
|
if (addr != IntPtr.Zero)
|
|
{
|
|
_executeSlotByIdHook = Plugin.GameInteropProvider.HookFromAddress<ExecuteSlotByIdDelegate>(addr, HandleExecuteSlotById);
|
|
_executeSlotByIdHook.Enable();
|
|
Plugin.Logger.Info("[HSUI] ExecuteSlotById hook installed (drag-drop overwrite protection)");
|
|
}
|
|
else
|
|
{
|
|
_executeSlotByIdHook = Plugin.GameInteropProvider.HookFromSignature<ExecuteSlotByIdDelegate>(
|
|
"4C 8B C9 41 83 F8 10 73 45",
|
|
HandleExecuteSlotById
|
|
);
|
|
_executeSlotByIdHook?.Enable();
|
|
Plugin.Logger.Info("[HSUI] ExecuteSlotById hook installed via signature");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Plugin.Logger.Error($"InputsHelper ExecuteSlotById Hook failed: {ex.Message}");
|
|
}
|
|
|
|
// mouseover setting
|
|
ConfigurationManager.Instance.ResetEvent += OnConfigReset;
|
|
Plugin.Framework.Update += OnFrameworkUpdate;
|
|
|
|
OnConfigReset(ConfigurationManager.Instance);
|
|
}
|
|
|
|
public static void Initialize() { Instance = new InputsHelper(); }
|
|
|
|
public static InputsHelper Instance { get; private set; } = null!;
|
|
|
|
public static int InitializationDelay = 5;
|
|
|
|
~InputsHelper()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Plugin.Logger.Info("\tDisposing InputsHelper...");
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected void Dispose(bool disposing)
|
|
{
|
|
if (!disposing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ConfigurationManager.Instance.ResetEvent -= OnConfigReset;
|
|
Plugin.Framework.Update -= OnFrameworkUpdate;
|
|
|
|
Plugin.Logger.Info("\t\tDisposing _requestActionHook: " + (_requestActionHook?.Address.ToString("X") ?? "null"));
|
|
_requestActionHook?.Disable();
|
|
_requestActionHook?.Dispose();
|
|
_executeSlotByIdHook?.Disable();
|
|
_executeSlotByIdHook?.Dispose();
|
|
|
|
// give imgui the control of inputs again
|
|
RestoreWndProc();
|
|
|
|
Instance = null!;
|
|
}
|
|
#endregion
|
|
|
|
private HUDOptionsConfig _config = null!;
|
|
|
|
//private Hook<OnSetUIMouseoverActor>? _uiMouseOverActorHook;
|
|
|
|
private Hook<UseActionDelegate>? _requestActionHook;
|
|
private Hook<ExecuteSlotByIdDelegate>? _executeSlotByIdHook;
|
|
|
|
private ExcelSheet<Action>? _sheet;
|
|
|
|
public bool HandlingMouseInputs { get; private set; } = false;
|
|
private IGameObject? _target = null;
|
|
private bool _ignoringMouseover = false;
|
|
|
|
public bool IsProxyEnabled => _config.InputsProxyEnabled;
|
|
|
|
public void ToggleProxy(bool enabled)
|
|
{
|
|
_config.InputsProxyEnabled = enabled;
|
|
ConfigurationManager.Instance.SaveConfigurations();
|
|
}
|
|
|
|
public void SetTarget(IGameObject? target, bool ignoreMouseover = false)
|
|
{
|
|
if (!IsProxyEnabled &&
|
|
ClipRectsHelper.Instance?.IsPointClipped(ImGui.GetMousePos()) == false)
|
|
{
|
|
ImGui.SetNextFrameWantCaptureMouse(true);
|
|
}
|
|
|
|
_target = target;
|
|
HandlingMouseInputs = true;
|
|
_ignoringMouseover = ignoreMouseover;
|
|
|
|
if (!_ignoringMouseover)
|
|
{
|
|
long address = _target != null && _target.GameObjectId != 0 ? (long)_target.Address : 0;
|
|
SetGameMouseoverTarget(address);
|
|
}
|
|
}
|
|
|
|
public void ClearTarget()
|
|
{
|
|
_target = null;
|
|
HandlingMouseInputs = false;
|
|
|
|
SetGameMouseoverTarget(0);
|
|
}
|
|
|
|
public void StartHandlingInputs()
|
|
{
|
|
HandlingMouseInputs = true;
|
|
}
|
|
|
|
public void StopHandlingInputs()
|
|
{
|
|
HandlingMouseInputs = false;
|
|
_ignoringMouseover = false;
|
|
}
|
|
|
|
private unsafe void SetGameMouseoverTarget(long address)
|
|
{
|
|
if (!_config.MouseoverEnabled || _config.MouseoverAutomaticMode || _ignoringMouseover)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UIModule* uiModule = Framework.Instance()->GetUIModule();
|
|
if (uiModule == null) { return; }
|
|
|
|
PronounModule* pronounModule = uiModule->GetPronounModule();
|
|
if (pronounModule == null) { return; }
|
|
|
|
pronounModule->UiMouseOverTarget = (GameObject*)address;
|
|
}
|
|
|
|
private void OnConfigReset(ConfigurationManager sender)
|
|
{
|
|
_config = sender.GetConfigObject<HUDOptionsConfig>();
|
|
}
|
|
|
|
//private void HandleUIMouseOverActorId(long arg1, long arg2)
|
|
//{
|
|
//Plugin.Logger.Log("MO: {0} - {1}", arg1.ToString("X"), arg2.ToString("X"));
|
|
//_uiMouseOverActorHook?.Original(arg1, arg2);
|
|
//}
|
|
|
|
private bool HandleRequestAction(
|
|
ActionManager* manager,
|
|
ActionType actionType,
|
|
uint actionId,
|
|
ulong targetId,
|
|
uint extraParam,
|
|
UseActionMode mode,
|
|
uint comboRouteId,
|
|
bool* outOptAreaTargeted
|
|
)
|
|
{
|
|
if (_requestActionHook == null) { return false; }
|
|
|
|
// Block UseAction when we just placed this action via drag-drop (game may execute from drop via path that bypasses WndProc)
|
|
var (suppressActionId, suppressUntil) = _suppressUseActionForDrop;
|
|
if (suppressActionId != 0 && actionId == suppressActionId && ImGui.GetTime() < suppressUntil)
|
|
{
|
|
if (IsActionBarDragDropDebugEnabled())
|
|
Plugin.Logger.Information($"[HSUI DragDrop DBG] UseAction SUPPRESSED actionId={actionId} (drop cooldown)");
|
|
_suppressUseActionForDrop = (0, 0);
|
|
return false;
|
|
}
|
|
if (ImGui.GetTime() >= suppressUntil)
|
|
_suppressUseActionForDrop = (0, 0);
|
|
|
|
if (_config.MouseoverEnabled &&
|
|
_config.MouseoverAutomaticMode &&
|
|
_target != null &&
|
|
IsActionValid(actionId, _target) &&
|
|
!_ignoringMouseover)
|
|
{
|
|
return _requestActionHook.Original(manager, actionType, actionId, _target.GameObjectId, extraParam, mode, comboRouteId, outOptAreaTargeted);
|
|
}
|
|
|
|
return _requestActionHook.Original(manager, actionType, actionId, targetId, extraParam, mode, comboRouteId, outOptAreaTargeted);
|
|
}
|
|
|
|
private byte HandleExecuteSlotById(RaptureHotbarModule* module, uint hotbarId, uint slotId)
|
|
{
|
|
var (suppressBar, suppressSlot, suppressUntil) = _suppressExecuteSlotByIdForDrop;
|
|
if (suppressBar < 10 && hotbarId == suppressBar && slotId == suppressSlot && ImGui.GetTime() < suppressUntil)
|
|
{
|
|
if (IsActionBarDragDropDebugEnabled())
|
|
Plugin.Logger.Information($"[HSUI DragDrop DBG] ExecuteSlotById SUPPRESSED bar={hotbarId} slot={slotId} (drop cooldown)");
|
|
_suppressExecuteSlotByIdForDrop = (99, 99, 0);
|
|
return 0;
|
|
}
|
|
if (ImGui.GetTime() >= suppressUntil)
|
|
_suppressExecuteSlotByIdForDrop = (99, 99, 0);
|
|
|
|
// Record for keypress flash (hotbarId 0-9 = bars 1-10, slotId 0-11)
|
|
LastExecutedBar = (int)hotbarId + 1;
|
|
LastExecutedSlot = (int)slotId;
|
|
LastExecutedTime = ImGui.GetTime();
|
|
|
|
return _executeSlotByIdHook != null ? _executeSlotByIdHook.Original(module, hotbarId, slotId) : (byte)0;
|
|
}
|
|
|
|
/// <summary>Bar 1-10, Slot 0-11. Set when ExecuteSlotById is invoked (keybind or click).</summary>
|
|
public static int LastExecutedBar { get; private set; } = -1;
|
|
public static int LastExecutedSlot { get; private set; } = -1;
|
|
public static double LastExecutedTime { get; private set; }
|
|
|
|
/// <summary>True if this slot was executed within the given duration (seconds).</summary>
|
|
public static bool WasSlotJustExecuted(int hotbarIndex, int slotIndex, double durationSec = 0.2)
|
|
{
|
|
if (LastExecutedBar != hotbarIndex || LastExecutedSlot != slotIndex) return false;
|
|
return (ImGui.GetTime() - LastExecutedTime) <= durationSec;
|
|
}
|
|
|
|
/// <summary>0=just pressed, 1=fully faded. For smooth flash decay.</summary>
|
|
public static float GetKeypressFlashAlpha(int hotbarIndex, int slotIndex, double durationSec = 0.2)
|
|
{
|
|
if (!WasSlotJustExecuted(hotbarIndex, slotIndex, durationSec)) return 0f;
|
|
double elapsed = ImGui.GetTime() - LastExecutedTime;
|
|
return (float)(1.0 - (elapsed / durationSec));
|
|
}
|
|
|
|
private bool IsActionValid(ulong actionID, IGameObject? target)
|
|
{
|
|
if (target == null || actionID == 0 || _sheet == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool found = _sheet.TryGetRow((uint)actionID, out Action action);
|
|
if (!found)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// handle actions that automatically switch to other actions
|
|
// ie GNB Continuation or SMN Egi Assaults
|
|
// these actions dont have an attack type or animation so in these cases
|
|
// we assume its a hostile spell
|
|
// if this doesn't work on all cases we can switch to a hardcoded list
|
|
// of special cases later
|
|
if (action.AttackType.RowId == 0 && action.AnimationStart.RowId == 0 &&
|
|
(!action.CanTargetAlly && !action.CanTargetHostile && !action.CanTargetParty && action.CanTargetSelf))
|
|
{
|
|
// special case for AST cards and SMN rekindle
|
|
if (actionID is 37019 or 37020 or 37021 or 25822)
|
|
{
|
|
return target is IPlayerCharacter or IBattleNpc { BattleNpcKind: BattleNpcSubKind.Chocobo };
|
|
}
|
|
|
|
return target is IBattleNpc npcTarget && npcTarget.BattleNpcKind == BattleNpcSubKind.Enemy;
|
|
}
|
|
|
|
// friendly player (TODO: pvp? lol)
|
|
if (target is IPlayerCharacter)
|
|
{
|
|
return action.CanTargetAlly || action.CanTargetParty || action.CanTargetSelf;
|
|
}
|
|
|
|
// friendly npc
|
|
if (target is IBattleNpc npc)
|
|
{
|
|
if (npc.BattleNpcKind != BattleNpcSubKind.Enemy)
|
|
{
|
|
return action.CanTargetAlly || action.CanTargetParty || action.CanTargetSelf;
|
|
}
|
|
}
|
|
|
|
return action.CanTargetHostile;
|
|
}
|
|
|
|
#region mouseover inputs proxy
|
|
private bool? _leftButtonClicked = null;
|
|
public bool LeftButtonClicked => _leftButtonClicked.HasValue ?
|
|
_leftButtonClicked.Value :
|
|
(IsProxyEnabled ? false : ImGui.IsMouseClicked(ImGuiMouseButton.Left));
|
|
|
|
private bool? _rightButtonClicked = null;
|
|
public bool RightButtonClicked => _rightButtonClicked.HasValue ?
|
|
_rightButtonClicked.Value :
|
|
(IsProxyEnabled ? false : ImGui.IsMouseClicked(ImGuiMouseButton.Right));
|
|
|
|
private bool _leftButtonWasDown = false;
|
|
private bool _rightButtonWasDown = false;
|
|
|
|
|
|
public void ClearClicks()
|
|
{
|
|
if (IsProxyEnabled)
|
|
{
|
|
WndProcDetour(_wndHandle, WM_LBUTTONUP, 0, 0);
|
|
WndProcDetour(_wndHandle, WM_RBUTTONUP, 0, 0);
|
|
}
|
|
}
|
|
|
|
// wnd proc detour
|
|
// if we're "eating" inputs, we only process left and right clicks
|
|
// any other message is passed along to the ImGui scene
|
|
private IntPtr WndProcDetour(IntPtr hWnd, uint msg, ulong wParam, long lParam)
|
|
{
|
|
// When the game has an active hotbar-relevant drag (Action, Macro, Item, etc.) AND the cursor is over
|
|
// an HSUI hotbar, eat LBUTTONUP so the game doesn't interpret it as a click on the (hidden) default
|
|
// hotbar and execute the ability. Do NOT eat when cursor is over game UI (Character Config, etc.) —
|
|
// the game uses the same icon/drag system for config submenus, so we must only intercept when we're
|
|
// actually dropping on our hotbars.
|
|
if (msg == WM_LBUTTONUP && IsHotbarRelevantGameDrag() && ActionBarsHitTestHelper.IsMouseOverAnyHSUIHotbar())
|
|
{
|
|
if (IsActionBarDragDropDebugEnabled())
|
|
Plugin.Logger.Information("[HSUI DragDrop DBG] WndProc: EATING LBUTTONUP (hotbar drag over HSUI bar)");
|
|
TryCancelGameDragDrop();
|
|
ImGui.GetIO().AddMouseButtonEvent((int)ImGuiMouseButton.Left, false);
|
|
return (IntPtr)0;
|
|
}
|
|
|
|
// eat left and right clicks?
|
|
if (HandlingMouseInputs && IsProxyEnabled)
|
|
{
|
|
switch (msg)
|
|
{
|
|
// mouse clicks
|
|
case WM_LBUTTONDOWN:
|
|
case WM_RBUTTONDOWN:
|
|
case WM_LBUTTONUP:
|
|
case WM_RBUTTONUP:
|
|
|
|
// if there's not a game window covering the cursor location
|
|
// we eat the message and handle the inputs manually
|
|
if (ClipRectsHelper.Instance?.IsPointClipped(ImGui.GetMousePos()) == false)
|
|
{
|
|
_leftButtonClicked = _leftButtonWasDown && msg == WM_LBUTTONUP;
|
|
_rightButtonClicked = _rightButtonWasDown && msg == WM_RBUTTONUP;
|
|
|
|
|
|
_leftButtonWasDown = msg == WM_LBUTTONDOWN;
|
|
_rightButtonWasDown = msg == WM_RBUTTONDOWN;
|
|
|
|
// never eat BUTTONUP messages to prevent clicks from getting stuck!!!
|
|
if (msg != WM_LBUTTONUP && msg != WM_RBUTTONUP)
|
|
{
|
|
// INPUT EATEN!!!
|
|
return (IntPtr)0;
|
|
}
|
|
}
|
|
// otherwise we let imgui handle the inputs
|
|
else
|
|
{
|
|
_leftButtonClicked = null;
|
|
_rightButtonClicked = null;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// call imgui's wnd proc
|
|
return (IntPtr)CallWindowProc(_imguiWndProcPtr, hWnd, msg, wParam, lParam);
|
|
}
|
|
|
|
public void OnFrameworkUpdate(IFramework framework)
|
|
{
|
|
// Keep WndProc hooked when: proxy mode (for mouseover) OR we need to block game drag
|
|
// release (so dropping on HSUI action bar doesn't execute the ability).
|
|
bool needHook = IsProxyEnabled || ShouldBlockGameDragRelease();
|
|
if (needHook && _wndProcPtr == IntPtr.Zero)
|
|
{
|
|
HookWndProc();
|
|
// Only log when we actually installed (HookWndProc can return early during init delay)
|
|
if (_wndProcPtr != IntPtr.Zero && IsActionBarDragDropDebugEnabled())
|
|
Plugin.Logger.Information("[HSUI DragDrop DBG] WndProc hook INSTALLED (needHook=true for drag-block)");
|
|
}
|
|
else if (!needHook && _wndProcPtr != IntPtr.Zero)
|
|
RestoreWndProc();
|
|
}
|
|
|
|
private static bool ShouldBlockGameDragRelease()
|
|
{
|
|
try
|
|
{
|
|
var hotbarsConfig = ConfigurationManager.Instance?.GetConfigObject<HSUI.Interface.GeneralElements.HotbarsConfig>();
|
|
return hotbarsConfig != null && hotbarsConfig.Enabled;
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
private static bool IsActionBarDragDropDebugEnabled()
|
|
{
|
|
try
|
|
{
|
|
var configs = ConfigurationManager.Instance?.GetObjects<HSUI.Interface.GeneralElements.HotbarBarConfig>();
|
|
return configs != null && configs.Exists(c => c.DebugDragDrop);
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
/// <summary>When we place an action via drag-drop, suppress the next UseAction for that action to prevent
|
|
/// the game from executing it (game may interpret drop as click via a path that bypasses WndProc).</summary>
|
|
private static (uint ActionId, double SuppressUntil) _suppressUseActionForDrop = (0, 0);
|
|
|
|
public static void SuppressUseActionAfterDrop(uint actionId, int durationMs = 300)
|
|
{
|
|
_suppressUseActionForDrop = (actionId, ImGui.GetTime() + durationMs / 1000.0);
|
|
}
|
|
|
|
/// <summary>Suppress ExecuteSlotById when we just placed via drag-drop (game hotbar click goes through this).</summary>
|
|
private static (uint HotbarId, uint SlotId, double SuppressUntil) _suppressExecuteSlotByIdForDrop = (99, 99, 0);
|
|
|
|
public static void SuppressExecuteSlotByIdAfterDrop(uint hotbarId, uint slotId, int durationMs = 300)
|
|
{
|
|
_suppressExecuteSlotByIdForDrop = (hotbarId, slotId, ImGui.GetTime() + durationMs / 1000.0);
|
|
}
|
|
|
|
public void OnFrameEnd()
|
|
{
|
|
_leftButtonClicked = null;
|
|
_rightButtonClicked = null;
|
|
}
|
|
|
|
private void HookWndProc()
|
|
{
|
|
if (Plugin.LoadTime <= 0 ||
|
|
ImGui.GetTime() - Plugin.LoadTime < InitializationDelay)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ulong processId = (ulong)Process.GetCurrentProcess().Id;
|
|
|
|
IntPtr hWnd = IntPtr.Zero;
|
|
do
|
|
{
|
|
hWnd = FindWindowExW(IntPtr.Zero, hWnd, "FFXIVGAME", null);
|
|
if (hWnd == IntPtr.Zero) { return; }
|
|
|
|
ulong wndProcessId = 0;
|
|
GetWindowThreadProcessId(hWnd, ref wndProcessId);
|
|
|
|
if (wndProcessId == processId)
|
|
{
|
|
break;
|
|
}
|
|
|
|
} while (hWnd != IntPtr.Zero);
|
|
|
|
if (hWnd == IntPtr.Zero) { return; }
|
|
|
|
_wndHandle = hWnd;
|
|
_wndProcDelegate = WndProcDetour;
|
|
_wndProcPtr = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate);
|
|
_imguiWndProcPtr = SetWindowLongPtr(hWnd, GWL_WNDPROC, _wndProcPtr);
|
|
|
|
Plugin.Logger.Info("Initializing HSUI Inputs v" + Plugin.Version);
|
|
Plugin.Logger.Info("\tHooking WndProc for window: " + hWnd.ToString("X"));
|
|
Plugin.Logger.Info("\tOld WndProc: " + _imguiWndProcPtr.ToString("X"));
|
|
}
|
|
|
|
private void RestoreWndProc()
|
|
{
|
|
if (_wndHandle != IntPtr.Zero && _imguiWndProcPtr != IntPtr.Zero)
|
|
{
|
|
Plugin.Logger.Info("\t\tRestoring WndProc");
|
|
Plugin.Logger.Info("\t\t\tOld _wndHandle = " + _wndHandle.ToString("X"));
|
|
Plugin.Logger.Info("\t\t\tOld _imguiWndProcPtr = " + _imguiWndProcPtr.ToString("X"));
|
|
|
|
SetWindowLongPtr(_wndHandle, GWL_WNDPROC, _imguiWndProcPtr);
|
|
Plugin.Logger.Info("\t\t\tDone!");
|
|
|
|
_wndHandle = IntPtr.Zero;
|
|
_imguiWndProcPtr = IntPtr.Zero;
|
|
}
|
|
}
|
|
|
|
private IntPtr _wndHandle = IntPtr.Zero;
|
|
private WndProcDelegate _wndProcDelegate = null!;
|
|
private IntPtr _wndProcPtr = IntPtr.Zero;
|
|
private IntPtr _imguiWndProcPtr = IntPtr.Zero;
|
|
|
|
public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, ulong wParam, long lParam);
|
|
|
|
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)]
|
|
public static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
|
|
|
|
[DllImport("user32.dll", EntryPoint = "CallWindowProcW")]
|
|
public static extern long CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, ulong wParam, long lParam);
|
|
|
|
[DllImport("user32.dll", EntryPoint = "FindWindowExW", SetLastError = true)]
|
|
public static extern IntPtr FindWindowExW(IntPtr hWndParent, IntPtr hWndChildAfter, [MarshalAs(UnmanagedType.LPWStr)] string? lpszClass, [MarshalAs(UnmanagedType.LPWStr)] string? lpszWindow);
|
|
|
|
[DllImport("user32.dll", EntryPoint = "GetWindowThreadProcessId", SetLastError = true)]
|
|
public static extern ulong GetWindowThreadProcessId(IntPtr hWnd, ref ulong id);
|
|
|
|
private const uint WM_LBUTTONDOWN = 513;
|
|
private const uint WM_LBUTTONUP = 514;
|
|
private const uint WM_RBUTTONDOWN = 516;
|
|
private const uint WM_RBUTTONUP = 517;
|
|
|
|
private const int GWL_WNDPROC = -4;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static unsafe bool IsGameDragDropActive()
|
|
{
|
|
try
|
|
{
|
|
var stage = AtkStage.Instance();
|
|
if (stage == null) return false;
|
|
var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
|
|
return dm != null && dm->IsDragging;
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
/// <summary>True when the game has an active drag that can be placed on a hotbar (Action, Macro, Item, etc.).
|
|
/// Used to avoid eating LBUTTONUP for other UI drags (e.g. Character Config menus).</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static unsafe bool IsHotbarRelevantGameDrag()
|
|
{
|
|
try
|
|
{
|
|
if (!IsGameDragDropActive()) return false;
|
|
var stage = AtkStage.Instance();
|
|
if (stage == null) return false;
|
|
var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
|
|
var dd = dm->DragDrop1;
|
|
if (dd == null) return false;
|
|
var slotType = UIGlobals.GetHotbarSlotTypeFromDragDropType(dd->DragDropType);
|
|
return slotType != RaptureHotbarModule.HotbarSlotType.Empty;
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
|
|
private static unsafe void TryCancelGameDragDrop()
|
|
{
|
|
try
|
|
{
|
|
var stage = AtkStage.Instance();
|
|
if (stage == null) return;
|
|
var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
|
|
if (dm != null)
|
|
dm->CancelDragDrop(true, true);
|
|
}
|
|
catch { /* best-effort */ }
|
|
}
|
|
#endregion
|
|
}
|
|
}
|